From 0f63c8a1867407a9cc474866f6f18f06dd4328b1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 28 Feb 2024 14:55:53 +0530 Subject: [PATCH 001/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] 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/219] fix: tag not shown as selected. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 61caf716f209..4f7e8b502a71 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1175,7 +1175,7 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledTags), + data: getTagsOptions(enabledAndSelectedTags), }); return tagSections; From 9321b99225a23576a0501e8106647d941ad06009 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 4 Apr 2024 00:33:07 +0700 Subject: [PATCH 020/219] 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 395468dc259e680429e64971479f72a40f2431a3 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 9 Apr 2024 11:39:38 +0200 Subject: [PATCH 021/219] 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 2e1f4ce29cca3c94151a02cc2f58a244517c25d2 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 9 Apr 2024 22:20:26 +0200 Subject: [PATCH 022/219] 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 023/219] 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 024/219] 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 025/219] 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 026/219] 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 027/219] 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 028/219] 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 029/219] 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 030/219] 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 031/219] 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 032/219] 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 033/219] 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 a743ffcb12f76928915760d1524fc9eb036489b6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Apr 2024 15:10:05 +0700 Subject: [PATCH 034/219] 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 035/219] 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 88a10fa97fe45013ea8ab916f4e62d17c18db4ce Mon Sep 17 00:00:00 2001 From: GandalfGwaihir Date: Fri, 19 Apr 2024 14:41:19 +0530 Subject: [PATCH 036/219] 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: Sat, 20 Apr 2024 18:39:39 +0530 Subject: [PATCH 037/219] fix: merge conflicts. Signed-off-by: Krishna Gupta --- src/components/TagPicker/index.tsx | 23 +- src/libs/OptionsListUtils.ts | 762 +++++++++++++++++++---------- 2 files changed, 529 insertions(+), 256 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index c071895cfdd9..97cd9aa5c691 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -4,8 +4,10 @@ import {withOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import type * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; @@ -13,7 +15,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; enabled: boolean; - accountID: number | null; + accountID: number | undefined; }; type TagPickerOnyxProps = { @@ -30,14 +32,14 @@ type TagPickerProps = TagPickerOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types policyID: string; - /** The selected tag of the money request */ + /** The selected tag of the expense */ selectedTag: string; /** The name of tag list we are getting tags for */ tagListName: string; /** Callback to submit the selected tag */ - onSubmit: () => void; + onSubmit: (selectedTag: Partial) => void; /** Should show the selected option that is disabled? */ shouldShowDisabledAndSelectedOption?: boolean; @@ -47,6 +49,7 @@ type TagPickerProps = TagPickerOnyxProps & { }; function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, onSubmit}: TagPickerProps) { + const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); @@ -66,8 +69,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe { name: selectedTag, enabled: true, - accountID: null, - isSelected: true, + accountID: undefined, }, ]; }, [selectedTag]); @@ -82,7 +84,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); const sections = useMemo( - () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, + () => OptionsListUtils.getFilteredOptions([], [], [], searchValue, selectedOptions, [], false, false, false, {}, [], true, enabledTags, policyRecentlyUsedTagsList, false).tagOptions, [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], ); @@ -92,15 +94,16 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe return ( ); } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 4f7e8b502a71..2aad4179c337 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -39,6 +39,7 @@ import times from '@src/utils/times'; import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; +import filterArrayByMatch from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -56,6 +57,15 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +type SearchOption = ReportUtils.OptionData & { + item: T; +}; + +type OptionList = { + reports: Array>; + personalDetails: Array>; +}; + type Option = Partial; /** @@ -89,22 +99,31 @@ type CategorySection = CategorySectionBase & { data: Option[]; }; +type TaxRatesOption = { + text?: string; + code?: string; + searchText?: string; + tooltipText?: string; + isDisabled?: boolean; + keyForList?: string; + data: Partial; +}; + +type TaxSection = { + title: string | undefined; + shouldShow: boolean; + data: TaxRatesOption[]; +}; + type CategoryTreeSection = CategorySectionBase & { data: OptionTree[]; + indexOffset?: number; }; type Category = { name: string; enabled: boolean; isSelected?: boolean; - applyDisabledStyle?: boolean; -}; - -type Tag = { - name: string; - enabled: boolean; - isSelected?: boolean; - applyDisabledStyle?: boolean; }; type Hierarchy = Record; @@ -140,6 +159,9 @@ type GetOptionsConfig = { includeSelectedOptions?: boolean; includeTaxRates?: boolean; taxRates?: TaxRatesWithDefault; + includePolicyReportFieldOptions?: boolean; + policyReportFieldOptions?: string[]; + recentlyUsedPolicyReportFieldOptions?: string[]; transactionViolations?: OnyxCollection; }; @@ -149,7 +171,7 @@ type MemberForList = { keyForList: string; isSelected: boolean; isDisabled: boolean; - accountID?: number | null; + accountID?: number; login: string; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; @@ -159,7 +181,7 @@ type MemberForList = { type SectionForSearchTerm = { section: CategorySection; }; -type GetOptions = { +type Options = { recentReports: ReportUtils.OptionData[]; personalDetails: ReportUtils.OptionData[]; userToInvite: ReportUtils.OptionData | null; @@ -167,9 +189,10 @@ type GetOptions = { categoryOptions: CategoryTreeSection[]; tagOptions: CategorySection[]; taxRatesOptions: CategorySection[]; + policyReportFieldOptions?: CategorySection[] | null; }; -type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -336,7 +359,7 @@ function isPersonalDetailsReady(personalDetails: OnyxEntry) /** * Get the participant option for a report. */ -function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxEntry): Participant { +function getParticipantsOption(participant: ReportUtils.OptionData | Participant, personalDetails: OnyxEntry): Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const login = detail?.login || participant.login || ''; @@ -517,6 +540,28 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu : ''; } +/** + * Update alternate text for the option when applicable + */ +function getAlternateText( + option: ReportUtils.OptionData, + {showChatPreviewLine = false, forcePolicyNamePreview = false, lastMessageTextFromReport = ''}: PreviewConfig & {lastMessageTextFromReport?: string}, +) { + if (!!option.isThread || !!option.isMoneyRequestReport) { + return lastMessageTextFromReport.length > 0 ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + if (!!option.isChatRoom || !!option.isPolicyExpenseChat) { + return showChatPreviewLine && !forcePolicyNamePreview && option.lastMessageText ? option.lastMessageText : option.subtitle; + } + if (option.isTaskReport) { + return showChatPreviewLine && option.lastMessageText ? option.lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } + + return showChatPreviewLine && option.lastMessageText + ? option.lastMessageText + : LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList[0].login ?? '' : ''); +} + /** * Get the last message text from the report directly or from other sources for special cases. */ @@ -554,7 +599,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); - lastMessageTextFromReport = ReportUtils.getReportPreviewMessage( + const reportPreviewMessage = ReportUtils.getReportPreviewMessage( !isEmptyObject(iouReport) ? iouReport : null, lastIOUMoneyReportAction, true, @@ -563,10 +608,11 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails true, lastReportAction, ); + lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(reportPreviewMessage); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { - lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report); + lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report, true); } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) { lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction); } else if (ReportActionUtils.isPendingRemove(lastReportAction) && ReportActionUtils.isThreadParentMessage(lastReportAction, report?.reportID ?? '')) { @@ -596,26 +642,27 @@ function createOption( personalDetails: OnyxEntry, report: OnyxEntry, reportActions: ReportActions, - {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, + config?: PreviewConfig, ): ReportUtils.OptionData { + const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {}; const result: ReportUtils.OptionData = { text: undefined, - alternateText: null, + alternateText: undefined, pendingAction: undefined, allReportErrors: undefined, brickRoadIndicator: null, icons: undefined, tooltipText: null, ownerAccountID: undefined, - subtitle: null, + subtitle: undefined, participantsList: undefined, accountID: 0, - login: null, + login: undefined, reportID: '', - phoneNumber: null, + phoneNumber: undefined, hasDraftComment: false, - keyForList: null, - searchText: null, + keyForList: undefined, + searchText: undefined, isDefaultRoom: false, isPinned: false, isWaitingOnBankAccount: false, @@ -630,6 +677,7 @@ function createOption( isExpenseReport: false, policyID: undefined, isOptimisticPersonalDetail: false, + lastMessageText: '', }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); @@ -638,10 +686,8 @@ function createOption( let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; - result.participantsList = personalDetailList; result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail; - if (report) { result.isChatRoom = ReportUtils.isChatRoom(report); result.isDefaultRoom = ReportUtils.isDefaultRoom(report); @@ -659,7 +705,6 @@ function createOption( result.ownerAccountID = report.ownerAccountID; result.reportID = report.reportID; result.isUnread = ReportUtils.isUnread(report); - result.hasDraftComment = report.hasDraft; result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); @@ -677,22 +722,21 @@ function createOption( let lastMessageText = lastMessageTextFromReport; const lastAction = visibleReportActionItems[report.reportID]; - const shouldDisplayLastActorName = lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU; + const shouldDisplayLastActorName = lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU; if (shouldDisplayLastActorName && lastActorDisplayName && lastMessageTextFromReport) { lastMessageText = `${lastActorDisplayName}: ${lastMessageTextFromReport}`; } - if (result.isThread || result.isMoneyRequestReport) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else if (result.isChatRoom || result.isPolicyExpenseChat) { - result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; - } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); - } else { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); - } - reportName = ReportUtils.getReportName(report); + result.lastMessageText = lastMessageText; + + // If displaying chat preview line is needed, let's overwrite the default alternate text + result.alternateText = + showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview, lastMessageTextFromReport}); + + reportName = showPersonalDetails + ? ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') + : ReportUtils.getReportName(report); } else { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); @@ -835,7 +879,7 @@ function getSearchValueForPhoneOrEmail(searchTerm: string) { * Verifies that there is at least one enabled option */ function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean { - return Object.values(options).some((option) => option.enabled); + return Object.values(options).some((option) => option.enabled && option.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } /** @@ -935,7 +979,6 @@ function getCategoryOptionTree(options: Record | Category[], i tooltipText: option.name, isDisabled: !option.enabled, isSelected: !!option.isSelected, - applyDisabledStyle: option.applyDisabledStyle, }); return; @@ -975,35 +1018,19 @@ function getCategoryListSections( maxRecentReportsToShow: number, ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); - const selectedOptionsWithDisabledStyle: Category[] = []; - const enabledCategoriesName: string[] = []; - const selectedOptionNames: string[] = []; - - const enabledCategories = Object.values(sortedCategories).filter((category) => { - if (category.enabled) { - enabledCategoriesName.push(category.name); - } - return category.enabled; - }); + const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - selectedOptions.forEach((option) => { - selectedOptionNames.push(option.name); - selectedOptionsWithDisabledStyle.push({ - ...option, - applyDisabledStyle: !enabledCategoriesName.includes(option.name), - }); - }); - - const enabledAndSelectedCategories = [...selectedOptionsWithDisabledStyle, ...enabledCategories]; const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; - if (numberOfEnabledCategories === 0 && selectedOptionsWithDisabledStyle.length > 0) { + if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptionsWithDisabledStyle, true), + data, + indexOffset: data.length, }); return categorySections; @@ -1012,43 +1039,50 @@ function getCategoryListSections( if (searchInputValue) { const searchCategories: Category[] = []; - enabledAndSelectedCategories.forEach((category) => { + enabledCategories.forEach((category) => { if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { return; } searchCategories.push({ ...category, - isSelected: selectedOptionNames.includes(category.name), + isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), }); }); + const data = getCategoryOptionTree(searchCategories, true); categorySections.push({ // "Search" section title: '', shouldShow: true, - data: getCategoryOptionTree(searchCategories, true), + data, + indexOffset: data.length, }); return categorySections; } - if (selectedOptionsWithDisabledStyle.length > 0) { + if (selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptionsWithDisabledStyle, true), + data, + indexOffset: data.length, }); } + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -1064,19 +1098,23 @@ function getCategoryListSections( if (filteredRecentlyUsedCategories.length > 0) { const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); + const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); categorySections.push({ // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getCategoryOptionTree(cutRecentlyUsedCategories, true), + data, + indexOffset: data.length, }); } + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -1087,7 +1125,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Tag[]): Option[] { +function getTagsOptions(tags: Array>, selectedOptions?: SelectedTagOption[]): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1097,8 +1135,7 @@ function getTagsOptions(tags: Tag[]): Option[] { searchText: tag.name, tooltipText: cleanedName, isDisabled: !tag.enabled, - isSelected: tag.isSelected, - applyDisabledStyle: tag.applyDisabledStyle, + isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name), }; }); } @@ -1115,67 +1152,46 @@ function getTagListSections( ) { const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; - const selectedOptionNames: string[] = []; - const enabledTagsName: string[] = []; - const selectedOptionsWithDisabledStyle: Category[] = []; - const enabledTags = sortedTags.filter((tag) => { - if (tag.enabled) { - enabledTagsName.push(tag.name); - } - return tag.enabled && !selectedOptionNames.includes(tag.name); - }); - selectedOptions.forEach((option) => { - selectedOptionNames.push(option.name); - selectedOptionsWithDisabledStyle.push({ - ...option, - applyDisabledStyle: !enabledTagsName.includes(option.name), - }); - }); - const enabledAndSelectedTags = [...selectedOptionsWithDisabledStyle, ...enabledTags]; - const numberEnabledOfTags = enabledTags.length; + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; + const numberOfTags = enabledTags.length; // If all tags are disabled but there's a previously selected tag, show only the selected tag - if (numberEnabledOfTags === 0 && selectedOptionsWithDisabledStyle.length > 0) { + if (numberOfTags === 0 && selectedOptions.length > 0) { + const selectedTagOptions = selectedOptions.map((option) => ({ + name: option.name, + // Should be marked as enabled to be able to be de-selected + enabled: true, + })); tagSections.push({ // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedOptionsWithDisabledStyle), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); return tagSections; } if (searchInputValue) { - const searchTags: Tag[] = []; - - enabledAndSelectedTags.forEach((tag) => { - if (!PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())) { - return; - } - - searchTags.push({ - ...tag, - isSelected: selectedOptionNames.includes(tag.name), - }); - }); + const searchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); tagSections.push({ // "Search" section title: '', shouldShow: true, - data: getTagsOptions(searchTags), + data: getTagsOptions(searchTags, selectedOptions), }); return tagSections; } - if (numberEnabledOfTags < CONST.TAG_LIST_THRESHOLD) { + if (numberOfTags < CONST.TAG_LIST_THRESHOLD) { tagSections.push({ // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledAndSelectedTags), + data: getTagsOptions(enabledTags, selectedOptions), }); return tagSections; @@ -1189,12 +1205,18 @@ function getTagListSections( .map((tag) => ({name: tag, enabled: true})); const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); - if (selectedOptionsWithDisabledStyle.length) { + if (selectedOptions.length) { + const selectedTagOptions = selectedOptions.map((option) => ({ + name: option.name, + // Should be marked as enabled to be able to unselect even though the selected category is disabled + enabled: true, + })); + tagSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedOptionsWithDisabledStyle), + data: getTagsOptions(selectedTagOptions, selectedOptions), }); } @@ -1205,7 +1227,7 @@ function getTagListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getTagsOptions(cutRecentlyUsedTags), + data: getTagsOptions(cutRecentlyUsedTags, selectedOptions), }); } @@ -1213,7 +1235,7 @@ function getTagListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getTagsOptions(filteredTags), + data: getTagsOptions(filteredTags, selectedOptions), }); return tagSections; @@ -1228,15 +1250,91 @@ function hasEnabledTags(policyTagList: Array return hasEnabledOptions(policyTagValueList); } +/** + * Transforms the provided report field options into option objects. + * + * @param reportFieldOptions - an initial report field options array + */ +function getReportFieldOptions(reportFieldOptions: string[]): Option[] { + return reportFieldOptions.map((name) => ({ + text: name, + keyForList: name, + searchText: name, + tooltipText: name, + isDisabled: false, + })); +} + +/** + * Build the section list for report field options + */ +function getReportFieldOptionsSection(options: string[], recentlyUsedOptions: string[], selectedOptions: Array>, searchInputValue: string) { + const reportFieldOptionsSections = []; + const selectedOptionKeys = selectedOptions.map(({text, keyForList, name}) => text ?? keyForList ?? name ?? '').filter((o) => !!o); + let indexOffset = 0; + + if (searchInputValue) { + const searchOptions = options.filter((option) => option.toLowerCase().includes(searchInputValue.toLowerCase())); + + reportFieldOptionsSections.push({ + // "Search" section + title: '', + shouldShow: true, + indexOffset, + data: getReportFieldOptions(searchOptions), + }); + + return reportFieldOptionsSections; + } + + const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((recentlyUsedOption) => !selectedOptionKeys.includes(recentlyUsedOption)); + const filteredOptions = options.filter((option) => !selectedOptionKeys.includes(option)); + + if (selectedOptionKeys.length) { + reportFieldOptionsSections.push({ + // "Selected" section + title: '', + shouldShow: true, + indexOffset, + data: getReportFieldOptions(selectedOptionKeys), + }); + + indexOffset += selectedOptionKeys.length; + } + + if (filteredRecentlyUsedOptions.length > 0) { + reportFieldOptionsSections.push({ + // "Recent" section + title: Localize.translateLocal('common.recent'), + shouldShow: true, + indexOffset, + data: getReportFieldOptions(filteredRecentlyUsedOptions), + }); + + indexOffset += filteredRecentlyUsedOptions.length; + } + + reportFieldOptionsSections.push({ + // "All" section when items amount more than the threshold + title: Localize.translateLocal('common.all'), + shouldShow: true, + indexOffset, + data: getReportFieldOptions(filteredOptions), + }); + + return reportFieldOptionsSections; +} + /** * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. * * @param taxRates - The original tax rates object. * @returns The transformed tax rates object.g */ -function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined, defaultKey?: string): Record { - const defaultTaxKey = defaultKey ?? taxRates?.defaultExternalID; - const getModifiedName = (data: TaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; +function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record { + const defaultTaxKey = taxRates?.defaultExternalID; + const getModifiedName = (data: TaxRate, code: string) => + `${data.name} (${data.value})${defaultTaxKey === code ? ` ${CONST.DOT_SEPARATOR} ${Localize.translateLocal('common.default')}` : ''}`; const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); return taxes; } @@ -1252,10 +1350,10 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] { /** * Builds the options for taxRates */ -function getTaxRatesOptions(taxRates: Array>): Option[] { +function getTaxRatesOptions(taxRates: Array>): TaxRatesOption[] { return taxRates.map((taxRate) => ({ text: taxRate.modifiedName, - keyForList: taxRate.code, + keyForList: taxRate.modifiedName, searchText: taxRate.modifiedName, tooltipText: taxRate.modifiedName, isDisabled: taxRate.isDisabled, @@ -1266,10 +1364,10 @@ function getTaxRatesOptions(taxRates: Array>): Option[] { /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string, defaultTaxKey?: string): CategorySection[] { +function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): TaxSection[] { const policyRatesSections = []; - const taxes = transformedTaxRates(taxRates, defaultTaxKey); + const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); @@ -1363,12 +1461,92 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } +function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { + const reportMapForAccountIDs: Record = {}; + const allReportOptions: Array> = []; + + if (reports) { + Object.values(reports).forEach((report) => { + if (!report) { + return; + } + + const isSelfDM = ReportUtils.isSelfDM(report); + // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. + const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; + + if (!accountIDs || accountIDs.length === 0) { + return; + } + + // Save the report in the map if this is a single participant so we can associate the reportID with the + // personal detail option later. Individuals should not be associated with single participant + // policyExpenseChats or chatRooms since those are not people. + if (accountIDs.length <= 1) { + reportMapForAccountIDs[accountIDs[0]] = report; + } + + allReportOptions.push({ + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }); + }); + } + + const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ + item: personalDetail, + ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), + })); + + return { + reports: allReportOptions, + personalDetails: allPersonalDetailsOptions as Array>, + }; +} + +function createOptionFromReport(report: Report, personalDetails: OnyxEntry) { + const accountIDs = report.participantAccountIDs ?? []; + + return { + item: report, + ...createOption(accountIDs, personalDetails, report, {}), + }; +} + /** - * Build the options + * Options need to be sorted in the specific order + * @param options - list of options to be sorted + * @param searchValue - search string + * @returns a sorted list of options + */ +function orderOptions(options: ReportUtils.OptionData[], searchValue: string | undefined) { + return lodashOrderBy( + options, + [ + (option) => { + if (!!option.isChatRoom || option.isArchivedRoom) { + return 3; + } + if (!option.login) { + return 2; + } + if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { + return 1; + } + + // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list + return 0; + }, + ], + ['asc'], + ); +} + +/** + * filter options based on specific conditions */ function getOptions( - reports: OnyxCollection, - personalDetails: OnyxEntry, + options: OptionList, { reportActions = {}, betas = [], @@ -1402,8 +1580,11 @@ function getOptions( includeTaxRates, taxRates, includeSelfDM = false, + includePolicyReportFieldOptions = false, + policyReportFieldOptions = [], + recentlyUsedPolicyReportFieldOptions = [], }: GetOptionsConfig, -): GetOptions { +): Options { if (includeCategories) { const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); @@ -1446,7 +1627,8 @@ function getOptions( }; } - if (!isPersonalDetailsReady(personalDetails)) { + if (includePolicyReportFieldOptions) { + const transformedPolicyReportFieldOptions = getReportFieldOptionsSection(policyReportFieldOptions, recentlyUsedPolicyReportFieldOptions, selectedOptions, searchInputValue); return { recentReports: [], personalDetails: [], @@ -1455,17 +1637,18 @@ function getOptions( categoryOptions: [], tagOptions: [], taxRatesOptions: [], + policyReportFieldOptions: transformedPolicyReportFieldOptions, }; } - let recentReportOptions = []; - let personalDetailsOptions: ReportUtils.OptionData[] = []; - const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase(); + const topmostReportId = Navigation.getTopmostReportId() ?? ''; // Filter out all the reports that shouldn't be displayed - const filteredReports = Object.values(reports ?? {}).filter((report) => { + const filteredReportOptions = options.reports.filter((option) => { + const report = option.item; + const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; @@ -1474,7 +1657,7 @@ function getOptions( return ReportUtils.shouldReportBeInOptionList({ report, - currentReportId: Navigation.getTopmostReportId() ?? '', + currentReportId: topmostReportId, betas, policies, doesReportHaveViolations, @@ -1487,27 +1670,28 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = lodashSortBy(filteredReports, (report) => { - if (ReportUtils.isArchivedRoom(report)) { + const orderedReportOptions = lodashSortBy(filteredReportOptions, (option) => { + const report = option.item; + if (option.isArchivedRoom) { return CONST.DATE.UNIX_EPOCH; } return report?.lastVisibleActionCreated; }); - orderedReports.reverse(); + orderedReportOptions.reverse(); + + const allReportOptions = orderedReportOptions.filter((option) => { + const report = option.item; - const allReportOptions: ReportUtils.OptionData[] = []; - orderedReports.forEach((report) => { if (!report) { return; } - const isThread = ReportUtils.isChatThread(report); - const isChatRoom = ReportUtils.isChatRoom(report); - const isTaskReport = ReportUtils.isTaskReport(report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const isSelfDM = ReportUtils.isSelfDM(report); + const isThread = option.isThread; + const isTaskReport = option.isTaskReport; + const isPolicyExpenseChat = option.isPolicyExpenseChat; + const isMoneyRequestReport = option.isMoneyRequestReport; + const isSelfDM = option.isSelfDM; // Currently, currentUser is not included in visibleChatMemberAccountIDs, so for selfDM we need to add the currentUser as participants. const accountIDs = isSelfDM ? [currentUserAccountID ?? 0] : report.visibleChatMemberAccountIDs ?? []; @@ -1536,7 +1720,7 @@ function getOptions( return; } - // In case user needs to add credit bank account, don't allow them to request more money from the workspace. + // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. if (includeOwnedWorkspaceChats && ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(report)) { return; } @@ -1545,33 +1729,11 @@ function getOptions( return; } - // Save the report in the map if this is a single participant so we can associate the reportID with the - // personal detail option later. Individuals should not be associated with single participant - // policyExpenseChats or chatRooms since those are not people. - if (accountIDs.length <= 1 && !isPolicyExpenseChat && !isChatRoom) { - reportMapForAccountIDs[accountIDs[0]] = report; - } - - allReportOptions.push( - createOption(accountIDs, personalDetails, report, reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + return option; }); - // We're only picking personal details that have logins set - // This is a temporary fix for all the logic that's been breaking because of the new privacy changes - // See https://github.com/Expensify/Expensify/issues/293465 for more context - // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText - const havingLoginPersonalDetails = !includeP2P - ? {} - : Object.fromEntries(Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail)); - let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => - createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { - showChatPreviewLine, - forcePolicyNamePreview, - }), - ); + + const havingLoginPersonalDetails = options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail); + let allPersonalDetailsOptions = havingLoginPersonalDetails; if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 @@ -1592,8 +1754,17 @@ function getOptions( optionsToExclude.push({login}); }); + let recentReportOptions = []; + let personalDetailsOptions: ReportUtils.OptionData[] = []; + if (includeRecentReports) { for (const reportOption of allReportOptions) { + /** + * By default, generated options does not have the chat preview line enabled. + * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. + */ + reportOption.alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview}); + // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; @@ -1681,7 +1852,7 @@ function getOptions( !isCurrentUser({login: searchValue} as PersonalDetails) && selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + (parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && !optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers @@ -1689,7 +1860,7 @@ function getOptions( // Generates an optimistic account ID for new users not yet saved in Onyx const optimisticAccountID = UserUtils.generateAccountID(searchValue); const personalDetailsExtended = { - ...personalDetails, + ...allPersonalDetails, [optimisticAccountID]: { accountID: optimisticAccountID, login: searchValue, @@ -1721,26 +1892,7 @@ function getOptions( // When sortByReportTypeInSearch is true, recentReports will be returned with all the reports including personalDetailsOptions in the correct Order. recentReportOptions.push(...personalDetailsOptions); personalDetailsOptions = []; - recentReportOptions = lodashOrderBy( - recentReportOptions, - [ - (option) => { - if (!!option.isChatRoom || option.isArchivedRoom) { - return 3; - } - if (!option.login) { - return 2; - } - if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { - return 1; - } - - // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list - return 0; - }, - ], - ['asc'], - ); + recentReportOptions = orderOptions(recentReportOptions, searchValue); } return { @@ -1757,10 +1909,10 @@ function getOptions( /** * Build the options for the Search view */ -function getSearchOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { +function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); - const options = getOptions(reports, personalDetails, { + const optionList = getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1779,11 +1931,11 @@ function getSearchOptions(reports: OnyxCollection, personalDetails: Onyx Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); - return options; + return optionList; } -function getShareLogOptions(reports: OnyxCollection, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { - return getOptions(reports, personalDetails, { +function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options { + return getOptions(options, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1834,8 +1986,8 @@ function getIOUConfirmationOptionsFromParticipants(participants: Array, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1853,29 +2005,38 @@ function getFilteredOptions( includeTaxRates = false, taxRates: TaxRatesWithDefault = {} as TaxRatesWithDefault, includeSelfDM = false, + includePolicyReportFieldOptions = false, + policyReportFieldOptions: string[] = [], + recentlyUsedPolicyReportFieldOptions: string[] = [], ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - includeRecentReports: true, - includePersonalDetails: true, - maxRecentReportsToShow: 5, - excludeLogins, - includeOwnedWorkspaceChats, - includeP2P, - includeCategories, - categories, - recentlyUsedCategories, - includeTags, - tags, - recentlyUsedTags, - canInviteUser, - includeSelectedOptions, - includeTaxRates, - taxRates, - includeSelfDM, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + includeRecentReports: true, + includePersonalDetails: true, + maxRecentReportsToShow: 5, + excludeLogins, + includeOwnedWorkspaceChats, + includeP2P, + includeCategories, + categories, + recentlyUsedCategories, + includeTags, + tags, + recentlyUsedTags, + canInviteUser, + includeSelectedOptions, + includeTaxRates, + taxRates, + includeSelfDM, + includePolicyReportFieldOptions, + policyReportFieldOptions, + recentlyUsedPolicyReportFieldOptions, + }, + ); } /** @@ -1883,8 +2044,8 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: Record, - personalDetails: OnyxEntry, + reports: Array> = [], + personalDetails: Array> = [], betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], @@ -1892,24 +2053,27 @@ function getShareDestinationOptions( includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { - return getOptions(reports, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - selectedOptions, - maxRecentReportsToShow: 0, // Unlimited - includeRecentReports: true, - includeMultipleParticipantReports: true, - includePersonalDetails: false, - showChatPreviewLine: true, - forcePolicyNamePreview: true, - includeThreads: true, - includeMoneyRequests: true, - includeTasks: true, - excludeLogins, - includeOwnedWorkspaceChats, - excludeUnknownUsers, - includeSelfDM: true, - }); + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + selectedOptions, + maxRecentReportsToShow: 0, // Unlimited + includeRecentReports: true, + includeMultipleParticipantReports: true, + includePersonalDetails: false, + showChatPreviewLine: true, + forcePolicyNamePreview: true, + includeThreads: true, + includeMoneyRequests: true, + includeTasks: true, + excludeLogins, + includeOwnedWorkspaceChats, + excludeUnknownUsers, + includeSelfDM: true, + }, + ); } /** @@ -1942,20 +2106,26 @@ function formatMemberForList(member: ReportUtils.OptionData): MemberForList { * Build the options for the Workspace Member Invite view */ function getMemberInviteOptions( - personalDetails: OnyxEntry, + personalDetails: Array>, betas: Beta[] = [], searchValue = '', excludeLogins: string[] = [], includeSelectedOptions = false, -): GetOptions { - return getOptions({}, personalDetails, { - betas, - searchInputValue: searchValue.trim(), - includePersonalDetails: true, - excludeLogins, - sortPersonalDetailsByAlphaAsc: true, - includeSelectedOptions, - }); + reports: Array> = [], + includeRecentReports = false, +): Options { + return getOptions( + {reports, personalDetails}, + { + betas, + searchInputValue: searchValue.trim(), + includePersonalDetails: true, + excludeLogins, + sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, + includeRecentReports, + }, + ); } /** @@ -2063,6 +2233,102 @@ function formatSectionsFromSearchTerm( }; } +/** + * Helper method to get the `keyForList` for the first option in the OptionsList + */ +function getFirstKeyForList(data?: Option[] | null) { + if (!data?.length) { + return ''; + } + + const firstNonEmptyDataObj = data[0]; + + return firstNonEmptyDataObj.keyForList ? firstNonEmptyDataObj.keyForList : ''; +} +/** + * Filters options based on the search input value + */ +function filterOptions(options: Options, searchInputValue: string): Options { + const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); + const searchTerms = searchValue ? searchValue.split(' ') : []; + + // The regex below is used to remove dots only from the local part of the user email (local-part@domain) + // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) + const emailRegex = /\.(?=[^\s@]*@)/g; + + const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => { + const keys: string[] = []; + const visibleChatMemberAccountIDs = item.participantsList ?? []; + if (allPersonalDetails) { + visibleChatMemberAccountIDs.forEach((participant) => { + const login = participant?.login; + + if (participant?.displayName) { + keys.push(participant.displayName); + } + + if (login) { + keys.push(login); + keys.push(login.replace(emailRegex, '')); + } + }); + } + + return keys; + }; + const matchResults = searchTerms.reduceRight((items, term) => { + const recentReports = filterArrayByMatch(items.recentReports, term, (item) => { + let values: string[] = []; + if (item.text) { + values.push(item.text); + } + + if (item.login) { + values.push(item.login); + values.push(item.login.replace(emailRegex, '')); + } + + if (item.isThread) { + if (item.alternateText) { + values.push(item.alternateText); + } + } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { + if (item.subtitle) { + values.push(item.subtitle); + } + } + values = values.concat(getParticipantsLoginsArray(item)); + + return uniqFast(values); + }); + const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) => + uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login ?? '', item.login?.replace(emailRegex, '') ?? '']), + ); + + return { + recentReports: recentReports ?? [], + personalDetails: personalDetails ?? [], + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; + }, options); + + const recentReports = matchResults.recentReports.concat(matchResults.personalDetails); + + return { + personalDetails: [], + recentReports: orderOptions(recentReports, searchValue), + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions: [], + taxRatesOptions: [], + }; +} + export { getAvatarsForAccountIDs, isCurrentUser, @@ -2095,8 +2361,12 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + filterOptions, + createOptionList, + createOptionFromReport, getReportOption, getTaxRatesSection, + getFirstKeyForList, }; -export type {MemberForList, CategorySection, GetOptions, PayeePersonalDetails, Category}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; From d7e3e5ce8c420d3ba5d16ed1e2c8cf0a633b1879 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 20 Apr 2024 18:45:47 +0530 Subject: [PATCH 038/219] remove redundant checks. Signed-off-by: Krishna Gupta --- src/components/SelectionList/RadioListItem.tsx | 2 +- src/components/SelectionList/types.ts | 3 --- src/libs/ReportUtils.ts | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx index 2690b1b47dd4..b38da6639970 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.tsx @@ -51,7 +51,7 @@ function RadioListItem({ isMultilineSupported ? styles.preWrap : styles.pre, item.alternateText ? styles.mb1 : null, /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ - (isDisabled || item.applyDisabledStyle) && styles.colorMuted, + isDisabled && styles.colorMuted, isMultilineSupported ? {paddingLeft} : null, ]} numberOfLines={isMultilineSupported ? 2 : 1} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index b06b8c1d528e..a96d6c3abb17 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -77,9 +77,6 @@ type ListItem = { /** Whether this option is disabled for selection */ isDisabled?: boolean | null; - /** To apply diabled style when item is not diabled to unselect */ - applyDisabledStyle?: boolean | null; - /** List title is bold by default. Use this props to customize it */ isBold?: boolean; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 612cad7a659e..af55b4ca29be 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -431,7 +431,6 @@ type OptionData = { descriptiveText?: string; notificationPreference?: NotificationPreference | null; isDisabled?: boolean | null; - applyDisabledStyle?: boolean | null; name?: string | null; isSelfDM?: boolean; reportID?: string; From 8551719144d331b71bbd52389a8a9845198f8d62 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 20 Apr 2024 21:33:35 +0530 Subject: [PATCH 039/219] make list item deselectable if disabled but has been selected. Signed-off-by: Krishna Gupta --- src/components/CategoryPicker.tsx | 1 - src/components/SelectionList/BaseListItem.tsx | 2 +- src/libs/OptionsListUtils.ts | 15 ++++++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index f1cab01bcc0f..d3dbdf28f2de 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -37,7 +37,6 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return [ { name: selectedCategory, - enabled: true, accountID: undefined, isSelected: true, }, diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 9e6fb31d0316..3b8e4e633d68 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -66,7 +66,7 @@ function BaseListItem({ {...bind} ref={pressableRef} onPress={() => onSelectRow(item)} - disabled={isDisabled} + disabled={isDisabled && !item.isSelected} accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2aad4179c337..4c60e049590c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1019,12 +1019,21 @@ function getCategoryListSections( ): CategoryTreeSection[] { const sortedCategories = sortCategories(categories); const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - + const enabledCategoriesNames = enabledCategories.map((category) => category.name); + const selectedOptionsWithDisabledState: Category[] = []; const categorySections: CategoryTreeSection[] = []; const numberOfEnabledCategories = enabledCategories.length; + selectedOptions.forEach((option) => { + if (enabledCategoriesNames.includes(option.name)) { + selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: true}); + return; + } + selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: false}); + }); + if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptions, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); categorySections.push({ // "Selected" section title: '', @@ -1062,7 +1071,7 @@ function getCategoryListSections( } if (selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptions, true); + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); categorySections.push({ // "Selected" section title: '', From d126d63a1bc71860e5fed59910ef2c6bf9884f88 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 21 Apr 2024 00:08:34 +0530 Subject: [PATCH 040/219] make list item deselectable if disabled but has been selected for tags & taxes. Signed-off-by: Krishna Gupta --- src/components/TagPicker/index.tsx | 1 + src/components/TaxPicker.tsx | 7 ++- src/libs/OptionsListUtils.ts | 70 +++++++++++++++--------------- src/types/onyx/Policy.ts | 3 ++ 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index 97cd9aa5c691..cbd9418a83e9 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -15,6 +15,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; enabled: boolean; + isSelected?: boolean; accountID: number | undefined; }; diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 0aed28681d5c..4871979a48cb 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -53,19 +53,18 @@ function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerPr return [ { - name: selectedTaxRate, - enabled: true, + modifiedName: selectedTaxRate, + isDisabled: false, accountID: null, }, ]; }, [selectedTaxRate]); - const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue), [taxRates, searchValue, selectedOptions]); + const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Tax[], searchValue), [taxRates, searchValue, selectedOptions]); const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(sections[0].data.length > 0, searchValue); const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]); - return ( ; type GetOptionsConfig = { @@ -1162,21 +1168,26 @@ function getTagListSections( const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))]; + const enabledTags = sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name)); + const enabledTagsNames = enabledTags.map((tag) => tag.name); + const selectedTagsWithDisabledState: SelectedTagOption[] = []; const numberOfTags = enabledTags.length; + selectedOptions.forEach((tag) => { + if (enabledTagsNames.includes(tag.name)) { + selectedTagsWithDisabledState.push({...tag, enabled: true}); + return; + } + selectedTagsWithDisabledState.push({...tag, enabled: false}); + }); + // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to be de-selected - enabled: true, - })); tagSections.push({ // "Selected" section title: '', shouldShow: false, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); return tagSections; @@ -1215,17 +1226,11 @@ function getTagListSections( const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); if (selectedOptions.length) { - const selectedTagOptions = selectedOptions.map((option) => ({ - name: option.name, - // Should be marked as enabled to be able to unselect even though the selected category is disabled - enabled: true, - })); - tagSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTagsOptions(selectedTagOptions, selectedOptions), + data: getTagsOptions(selectedTagsWithDisabledState, selectedOptions), }); } @@ -1367,33 +1372,39 @@ function getTaxRatesOptions(taxRates: Array>): TaxRatesOption[] tooltipText: taxRate.modifiedName, isDisabled: taxRate.isDisabled, data: taxRate, + isSelected: taxRate.isSelected, })); } /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): TaxSection[] { +function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Tax[], searchInputValue: string): TaxSection[] { const policyRatesSections = []; const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); + const enabledTaxRatesNames = enabledTaxRates.map((tax) => tax.modifiedName); + const selectedTaxRateWithDisabledState: Tax[] = []; const numberOfTaxRates = enabledTaxRates.length; + selectedOptions.forEach((tax) => { + if (enabledTaxRatesNames.includes(tax.modifiedName)) { + selectedTaxRateWithDisabledState.push({...tax, isDisabled: false, isSelected: true}); + return; + } + selectedTaxRateWithDisabledState.push({...tax, isDisabled: true, isSelected: true}); + }); + // If all tax are disabled but there's a previously selected tag, show only the selected tag if (numberOfTaxRates === 0 && selectedOptions.length > 0) { - const selectedTaxRateOptions = selectedOptions.map((option) => ({ - modifiedName: option.name, - // Should be marked as enabled to be able to be de-selected - isDisabled: false, - })); policyRatesSections.push({ // "Selected" sectiong title: '', shouldShow: false, - data: getTaxRatesOptions(selectedTaxRateOptions), + data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); return policyRatesSections; @@ -1423,24 +1434,15 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO return policyRatesSections; } - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); const filteredTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName && !selectedOptionNames.includes(taxRate.modifiedName)); if (selectedOptions.length > 0) { - const selectedTaxRatesOptions = selectedOptions.map((option) => { - const taxRateObject = Object.values(taxes).find((taxRate) => taxRate.modifiedName === option.name); - - return { - modifiedName: option.name, - isDisabled: !!taxRateObject?.isDisabled, - }; - }); - policyRatesSections.push({ // "Selected" section title: '', shouldShow: true, - data: getTaxRatesOptions(selectedTaxRatesOptions), + data: getTaxRatesOptions(selectedTaxRateWithDisabledState), }); } @@ -1623,7 +1625,7 @@ function getOptions( } if (includeTaxRates) { - const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Category[], searchInputValue); + const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Tax[], searchInputValue); return { recentReports: [], @@ -2378,4 +2380,4 @@ export { getFirstKeyForList, }; -export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, Tax, TaxRatesOption, Option, OptionTree}; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index c55d6359c68f..4a4526daafad 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -51,6 +51,9 @@ type TaxRate = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Indicates if the tax rate is disabled. */ isDisabled?: boolean; + /** Indicates if the tax rate is selected. */ + isSelected?: boolean; + /** An error message to display to the user */ errors?: OnyxCommon.Errors; From 4ebf5941363f59dd8acc6bbd9d16bb18729af404 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 21 Apr 2024 00:20:47 +0530 Subject: [PATCH 041/219] fix: seleceted category & tax not showing in search results. Signed-off-by: Krishna Gupta --- src/components/CategoryPicker.tsx | 1 + src/components/TaxPicker.tsx | 1 + src/libs/OptionsListUtils.ts | 11 +++++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index d3dbdf28f2de..3e62af29d16d 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -70,6 +70,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC }, [policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, policyCategories]); const selectedOptionKey = useMemo(() => (sections?.[0]?.data ?? []).filter((category) => category.searchText === selectedCategory)[0]?.keyForList, [sections, selectedCategory]); + return ( 0, searchValue); const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]); + return ( { + categoriesForSearch.forEach((category) => { if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { return; } @@ -1168,7 +1169,7 @@ function getTagListSections( const tagSections = []; const sortedTags = sortTags(tags) as PolicyTag[]; const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const enabledTags = sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name)); + const enabledTags = sortedTags.filter((tag) => tag.enabled); const enabledTagsNames = enabledTags.map((tag) => tag.name); const selectedTagsWithDisabledState: SelectedTagOption[] = []; const numberOfTags = enabledTags.length; @@ -1194,13 +1195,15 @@ function getTagListSections( } if (searchInputValue) { - const searchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const enabledSearchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; tagSections.push({ // "Search" section title: '', shouldShow: true, - data: getTagsOptions(searchTags, selectedOptions), + data: getTagsOptions(tagsForSearch, selectedOptions), }); return tagSections; From 77ef74aa2016921593e226693369a5bb3aba3028 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 22 Apr 2024 11:53:54 +0200 Subject: [PATCH 042/219] cleanup --- src/components/ReportActionItem/ReportPreview.tsx | 4 ++-- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/pages/home/report/ReportActionsList.tsx | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 802231a6c030..f969bc35a138 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -228,8 +228,8 @@ function ReportPreview({ const shouldShowSingleRequestMerchantOrDescription = numberOfRequests === 1 && (!!formattedMerchant || !!formattedDescription) && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend); const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchantOrDescription || numberOfRequests > 1); - const shouldShowScanningSubtitle = numberOfScanningReceipts === 1 && allTransactions.length === 1; - const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && allTransactions.length === 1; + const shouldShowScanningSubtitle = numberOfScanningReceipts === 1 && numberOfRequests === 1; + const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && numberOfRequests === 1; const {isSupportTextHtml, supportText} = useMemo(() => { if (formattedMerchant) { diff --git a/src/languages/en.ts b/src/languages/en.ts index 300bb8c19a0a..229baa46a8bc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -629,7 +629,7 @@ export default { posted: 'Posted', deleteReceipt: 'Delete receipt', routePending: 'Route pending...', - receiptScanning: 'Receipt scanning…', + 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', diff --git a/src/languages/es.ts b/src/languages/es.ts index 294402f37d35..e2c8c1fe3064 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -622,7 +622,7 @@ export default { posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', routePending: 'Ruta pendiente...', - receiptScanning: 'Escaneando recibo…', + 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', diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 3237554b69aa..19204958dd5f 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -194,7 +194,6 @@ function ReportActionsList({ ), [sortedReportActions, isOffline], ); - const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); const hasNewestReportAction = sortedReportActions?.[0].created === report.lastVisibleActionCreated; From 6c40f88a515b14ebf8ac34a2b85718a66803ff8a Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 22 Apr 2024 12:13:51 +0200 Subject: [PATCH 043/219] cleanup --- ios/NewExpensify.xcodeproj/project.pbxproj | 16 ++-- ios/Podfile.lock | 103 +++++++++++---------- 2 files changed, 64 insertions(+), 55 deletions(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index d9ec8cccee83..7f50db5da85a 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -626,11 +626,11 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipAutomationResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipCoreResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipExtendedActionsResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipMessageCenterResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipPreferenceCenterResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", @@ -639,11 +639,11 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipAutomationResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipCoreResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipExtendedActionsResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipMessageCenterResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipPreferenceCenterResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", @@ -789,11 +789,11 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipAutomationResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipCoreResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipExtendedActionsResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipMessageCenterResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipPreferenceCenterResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", @@ -802,11 +802,11 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipAutomationResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipCoreResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipExtendedActionsResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipMessageCenterResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipPreferenceCenterResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2964b8d72c06..b59f4ef01158 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,26 +1,25 @@ PODS: - - Airship (16.12.1): - - Airship/Automation (= 16.12.1) - - Airship/Basement (= 16.12.1) - - Airship/Core (= 16.12.1) - - Airship/ExtendedActions (= 16.12.1) - - Airship/MessageCenter (= 16.12.1) - - Airship/Automation (16.12.1): + - Airship (17.7.3): + - Airship/Automation (= 17.7.3) + - Airship/Basement (= 17.7.3) + - Airship/Core (= 17.7.3) + - Airship/FeatureFlags (= 17.7.3) + - Airship/MessageCenter (= 17.7.3) + - Airship/PreferenceCenter (= 17.7.3) + - Airship/Automation (17.7.3): - Airship/Core - - Airship/Basement (16.12.1) - - Airship/Core (16.12.1): + - Airship/Basement (17.7.3) + - Airship/Core (17.7.3): - Airship/Basement - - Airship/ExtendedActions (16.12.1): + - Airship/FeatureFlags (17.7.3): - Airship/Core - - Airship/MessageCenter (16.12.1): + - Airship/MessageCenter (17.7.3): - Airship/Core - - Airship/PreferenceCenter (16.12.1): + - Airship/PreferenceCenter (17.7.3): - Airship/Core - - AirshipFrameworkProxy (2.1.1): - - Airship (= 16.12.1) - - Airship/MessageCenter (= 16.12.1) - - Airship/PreferenceCenter (= 16.12.1) - - AirshipServiceExtension (17.7.3) + - AirshipFrameworkProxy (5.1.1): + - Airship (= 17.7.3) + - AirshipServiceExtension (17.8.0) - AppAuth (1.6.2): - AppAuth/Core (= 1.6.2) - AppAuth/ExternalUserAgent (= 1.6.2) @@ -160,34 +159,44 @@ PODS: - GoogleUtilities/Network (~> 7.4) - "GoogleUtilities/NSData+zlib (~> 7.4)" - nanopb (~> 2.30908.0) - - GoogleDataTransport (9.3.0): + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - GoogleSignIn (7.0.0): - AppAuth (~> 1.5) - GTMAppAuth (< 3.0, >= 1.3) - GTMSessionFetcher/Core (< 4.0, >= 1.1) - - GoogleUtilities/AppDelegateSwizzler (7.12.0): + - GoogleUtilities/AppDelegateSwizzler (7.13.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/ISASwizzler (7.12.0) - - GoogleUtilities/Logger (7.12.0): + - GoogleUtilities/ISASwizzler (7.13.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (7.13.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (7.13.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.12.0)" - - GoogleUtilities/Reachability (7.12.0): + - "GoogleUtilities/NSData+zlib (7.13.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.0) + - GoogleUtilities/Reachability (7.13.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.0): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - GTMAppAuth (2.0.0): - AppAuth/Core (~> 1.6) - GTMSessionFetcher/Core (< 4.0, >= 1.5) @@ -238,12 +247,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - MapboxCommon (23.9.0) - - MapboxCoreMaps (10.16.5): + - MapboxCommon (23.9.1) + - MapboxCoreMaps (10.16.6): - MapboxCommon (~> 23.9) - - MapboxMaps (10.16.5): - - MapboxCommon (= 23.9.0) - - MapboxCoreMaps (= 10.16.5) + - MapboxMaps (10.16.6): + - MapboxCommon (= 23.9.1) + - MapboxCoreMaps (= 10.16.6) - MapboxMobileEvents (= 1.0.10) - Turf (= 2.7.0) - MapboxMobileEvents (1.0.10) @@ -1152,8 +1161,8 @@ PODS: - React-Mapbuffer (0.73.4): - glog - React-debug - - react-native-airship (15.3.1): - - AirshipFrameworkProxy (= 2.1.1) + - react-native-airship (17.2.1): + - AirshipFrameworkProxy (= 5.1.1) - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1227,7 +1236,7 @@ PODS: - React-Codegen - React-Core - ReactCommon/turbomodule/core - - react-native-geolocation (3.0.6): + - react-native-geolocation (3.2.1): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2406,9 +2415,9 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - Airship: 2f4510b497a8200780752a5e0304a9072bfffb6d - AirshipFrameworkProxy: ea1b6c665c798637b93c465b5e505be3011f1d9d - AirshipServiceExtension: 55730cc24d595847c1550b8d3cf62b807742d560 + Airship: 5a6d3f8a982398940b0d48423bb9b8736717c123 + AirshipFrameworkProxy: 7255f4ed9836dc2920f2f1ea5657ced4cee8a35c + AirshipServiceExtension: 0a5fb14c3fd1879355ab05a81d10f64512a4f79c AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 boost: d3f49c53809116a5d38da093a8aa78bf551aed09 BVLinearGradient: 421743791a59d259aec53f4c58793aad031da2ca @@ -2432,9 +2441,9 @@ SPEC CHECKSUMS: fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 - GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleSignIn: b232380cf495a429b8095d3178a8d5855b42e842 - GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae GTMSessionFetcher: 8a1b34ad97ebe6f909fb8b9b77fba99943007556 hermes-engine: b2669ce35fc4ac14f523b307aff8896799829fe2 @@ -2445,9 +2454,9 @@ SPEC CHECKSUMS: libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 lottie-ios: 3d98679b41fa6fd6aff2352b3953dbd3df8a397e lottie-react-native: 80bda323805fa62005afff0583d2927a89108f20 - MapboxCommon: e89c490cccd7ea9efcdc0e74b4ce1dbd9b4a875a - MapboxCoreMaps: 920f194f4f8b37f5731e0bdb82296d96ac4276f5 - MapboxMaps: e8d94fb4782295df68172ce7ed567da8228c629e + MapboxCommon: 20466d839cc43381c44df09d19f7f794b55b9a93 + MapboxCoreMaps: c21f433decbb295874f0c2464e492166db813b56 + MapboxMaps: c3b36646b9038706bbceb5de203bcdd0f411e9d0 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062 @@ -2476,12 +2485,12 @@ SPEC CHECKSUMS: React-jsitracing: e8a2dafb9878dbcad02b6b2b88e66267fb427b74 React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad - react-native-airship: 2ed75ff2278f11ff1c1ab08ed68f5bf02727b971 + react-native-airship: 6ab7a7974d53f92b0c46548fc198f797fdbf371f react-native-blob-util: a3ee23cfdde79c769c138d505670055de233b07a react-native-cameraroll: 95ce0d1a7d2d1fe55bf627ab806b64de6c3e69e9 react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 - react-native-geolocation: dcc37809bc117ffdb5946fecc127d62319ccd4a9 + react-native-geolocation: c1c21a8cda4abae6724a322458f64ac6889b8c2b react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: 74d18ad516037536c2f671ef0914bcce7739b2f5 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d @@ -2549,7 +2558,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d From 02900903900e4227a39d583d00717cab53a3e48f Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Mon, 22 Apr 2024 12:14:32 +0200 Subject: [PATCH 044/219] cleanup --- src/languages/es.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2488d7cb56c9..df28db6efbd6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -733,11 +733,8 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', -<<<<<<< HEAD transactionPending: 'Transacción pendiente.', -======= chooseARate: ({unit}: ReimbursementRateParams) => `Seleccione una tasa de reembolso del espacio de trabajo por ${unit}`, ->>>>>>> origin/main }, notificationPreferencesPage: { header: 'Preferencias de avisos', From 164d56e4f179dca8dcfab189e4039e47be26c83f Mon Sep 17 00:00:00 2001 From: GandalfGwaihir Date: Tue, 23 Apr 2024 03:53:48 +0530 Subject: [PATCH 045/219] Fix skeleton of Personal details profile --- .../CurrentUserPersonalDetailsSkeletonView/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx index 367e54e8be64..86109c9d6d53 100644 --- a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx +++ b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx @@ -37,8 +37,8 @@ function CurrentUserPersonalDetailsSkeletonView({shouldAnimate = true, avatarSiz Date: Tue, 23 Apr 2024 05:37:46 +0700 Subject: [PATCH 046/219] fix rename isInGSDMode --- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 8 ++++---- src/libs/SidebarUtils.ts | 6 +++--- src/libs/UnreadIndicatorUpdater/index.ts | 2 +- src/libs/actions/Report.ts | 2 +- tests/perf-test/ReportUtils.perf-test.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 2aad4179c337..2395095a2161 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1661,7 +1661,7 @@ function getOptions( betas, policies, doesReportHaveViolations, - isInGSDMode: false, + isInFocusMode: false, excludeEmptyChats: false, includeSelfDM, }); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b66f94801ca4..fc5375c4960c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4765,7 +4765,7 @@ function hasViolations(reportID: string, transactionViolations: OnyxCollection; currentReportId: string; - isInGSDMode: boolean; + isInFocusMode: boolean; betas: OnyxEntry; policies: OnyxCollection; excludeEmptyChats: boolean; doesReportHaveViolations: boolean; includeSelfDM?: boolean; }) { - const isInDefaultMode = !isInGSDMode; + const isInDefaultMode = !isInFocusMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. // This can happen if data is currently loading from the server or a report is in various stages of being created. // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. @@ -4859,7 +4859,7 @@ function shouldReportBeInOptionList({ } // All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones - if (isInGSDMode) { + if (isInFocusMode) { return isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index a218534e6b16..64addf5e7011 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -71,8 +71,8 @@ function getOrderedReportIDs( currentPolicyID = '', policyMemberAccountIDs: number[] = [], ): string[] { - const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; - const isInDefaultMode = !isInGSDMode; + const isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD; + const isInDefaultMode = !isInFocusMode; const allReportsDictValues = Object.values(allReports ?? {}); // Filter out all the reports that shouldn't be displayed @@ -102,7 +102,7 @@ function getOrderedReportIDs( return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', - isInGSDMode, + isInFocusMode, betas, policies, excludeEmptyChats: true, diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index b4f3cd34a8c4..13ea48fe8683 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -19,7 +19,7 @@ export default function getUnreadReportsForUnreadIndicator(reports: OnyxCollecti betas: [], policies: {}, doesReportHaveViolations: false, - isInGSDMode: false, + isInFocusMode: false, excludeEmptyChats: false, }) && /** diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8b0dbf8a37a9..e6e0ce294e06 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2455,7 +2455,7 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { ReportUtils.shouldReportBeInOptionList({ report: sortedReport, currentReportId: '', - isInGSDMode: false, + isInFocusMode: false, betas: [], policies: {}, excludeEmptyChats: true, diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index 8a5c5d6ff935..4130b6788894 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -136,13 +136,13 @@ describe('ReportUtils', () => { test('[ReportUtils] shouldReportBeInOptionList on 1k participant', async () => { const report = {...createRandomReport(1), participantAccountIDs, type: CONST.REPORT.TYPE.CHAT}; const currentReportId = '2'; - const isInGSDMode = true; + const isInFocusMode = true; const betas = [CONST.BETAS.DEFAULT_ROOMS]; const policies = getMockedPolicies(); await waitForBatchedUpdates(); await measureFunction(() => - ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInGSDMode, betas, policies, doesReportHaveViolations: false, excludeEmptyChats: false}), + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies, doesReportHaveViolations: false, excludeEmptyChats: false}), ); }); From e84492c68697052108b567c8e6a8c855b925a916 Mon Sep 17 00:00:00 2001 From: GandalfGwaihir Date: Tue, 23 Apr 2024 04:22:31 +0530 Subject: [PATCH 047/219] Add currentUserpersonal skeleton --- .../CurrentUserPersonalDetailsSkeletonView/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx index 86109c9d6d53..21e82c26f769 100644 --- a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx +++ b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx @@ -15,15 +15,9 @@ type CurrentUserPersonalDetailsSkeletonViewProps = { /** The size of the avatar */ avatarSize?: ValueOf; - - /** Background color of the skeleton view */ - backgroundColor?: string; - - /** Foreground color of the skeleton view */ - foregroundColor?: string; }; -function CurrentUserPersonalDetailsSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.LARGE, backgroundColor, foregroundColor}: CurrentUserPersonalDetailsSkeletonViewProps) { +function CurrentUserPersonalDetailsSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.LARGE}: CurrentUserPersonalDetailsSkeletonViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); From 8fd696e63cb28bd3b9d64ead3e03658b2c755879 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 24 Apr 2024 17:28:05 +0530 Subject: [PATCH 048/219] fix: dubplicate taxes shown & selected disabled tax not showing when searched. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f951402887ac..713eb9df716e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1388,8 +1388,10 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO const taxes = transformedTaxRates(taxRates); const sortedTaxRates = sortTaxRates(taxes); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); const enabledTaxRatesNames = enabledTaxRates.map((tax) => tax.modifiedName); + const enabledTaxRatesWithoutSelectedOptions = enabledTaxRates.filter((tax) => tax.modifiedName && !selectedOptionNames.includes(tax.modifiedName)); const selectedTaxRateWithDisabledState: Tax[] = []; const numberOfTaxRates = enabledTaxRates.length; @@ -1414,13 +1416,15 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO } if (searchInputValue) { - const searchTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const enabledSearchTaxRates = enabledTaxRatesWithoutSelectedOptions.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const selectedSearchTags = selectedTaxRateWithDisabledState.filter((taxRate) => taxRate.modifiedName?.toLowerCase().includes(searchInputValue.toLowerCase())); + const taxesForSearch = [...selectedSearchTags, ...enabledSearchTaxRates]; policyRatesSections.push({ // "Search" section title: '', shouldShow: true, - data: getTaxRatesOptions(searchTaxRates), + data: getTaxRatesOptions(taxesForSearch), }); return policyRatesSections; @@ -1431,15 +1435,12 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTaxRatesOptions(enabledTaxRates), + data: getTaxRatesOptions([...selectedTaxRateWithDisabledState, ...enabledTaxRatesWithoutSelectedOptions]), }); return policyRatesSections; } - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); - const filteredTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName && !selectedOptionNames.includes(taxRate.modifiedName)); - if (selectedOptions.length > 0) { policyRatesSections.push({ // "Selected" section @@ -1453,7 +1454,7 @@ function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedO // "All" section when number of items are more than the threshold title: '', shouldShow: true, - data: getTaxRatesOptions(filteredTaxRates), + data: getTaxRatesOptions(enabledTaxRatesWithoutSelectedOptions), }); return policyRatesSections; From 53fecfaff70cf0d44af7394108e4d8799d346ce8 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 24 Apr 2024 19:05:46 +0700 Subject: [PATCH 049/219] fix update comment --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fc5375c4960c..a8b4bd7b06d5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4616,7 +4616,7 @@ function buildOptimisticMoneyRequestEntities( return [createdActionForChat, createdActionForIOUReport, iouAction, transactionThread, createdActionForTransactionThread]; } -// Check if the report is empty report +// Check if the report is empty, meaning it has no visible messages (i.e. only a "created" report action). function isEmptyReport(report: OnyxEntry): boolean { if (!report) { return true; From c03ded65d326535a5d1f2e62c26761e4f4fbf0d9 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 24 Apr 2024 17:46:13 +0530 Subject: [PATCH 050/219] fix: Tag - duplicate selected options shown and seleceted disbaled option not shown when searched Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 713eb9df716e..33854931349b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1171,6 +1171,7 @@ function getTagListSections( const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const enabledTags = sortedTags.filter((tag) => tag.enabled); const enabledTagsNames = enabledTags.map((tag) => tag.name); + const enabledTagsWithoutSelectedOptions = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); const selectedTagsWithDisabledState: SelectedTagOption[] = []; const numberOfTags = enabledTags.length; @@ -1195,7 +1196,7 @@ function getTagListSections( } if (searchInputValue) { - const enabledSearchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + const enabledSearchTags = enabledTagsWithoutSelectedOptions.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); const selectedSearchTags = selectedTagsWithDisabledState.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); const tagsForSearch = [...selectedSearchTags, ...enabledSearchTags]; @@ -1214,7 +1215,7 @@ function getTagListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getTagsOptions(enabledTags, selectedOptions), + data: getTagsOptions([...selectedTagsWithDisabledState, ...enabledTagsWithoutSelectedOptions], selectedOptions), }); return tagSections; @@ -1226,7 +1227,6 @@ function getTagListSections( return !!tagObject?.enabled && !selectedOptionNames.includes(recentlyUsedTag); }) .map((tag) => ({name: tag, enabled: true})); - const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); if (selectedOptions.length) { tagSections.push({ @@ -1252,7 +1252,7 @@ function getTagListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getTagsOptions(filteredTags, selectedOptions), + data: getTagsOptions(enabledTagsWithoutSelectedOptions, selectedOptions), }); return tagSections; From 34a857a41cd3ecf2cd1ac8c37eddd2ee4b41a82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Wed, 24 Apr 2024 16:20:56 +0200 Subject: [PATCH 051/219] added missing padding --- src/pages/settings/Wallet/ReportCardLostPage.tsx | 13 ++++++++----- .../settings/Wallet/ReportVirtualCardFraudPage.tsx | 5 ++++- src/pages/settings/Wallet/TransferBalancePage.tsx | 3 +++ src/pages/tasks/NewTaskPage.tsx | 5 ++++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index 0c5c0a5c9ff2..e8f1149779b0 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -11,6 +11,7 @@ import SingleOptionSelector from '@components/SingleOptionSelector'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -94,6 +95,8 @@ function ReportCardLostPage({ const prevIsLoading = usePrevious(formData?.isLoading); + const {paddingBottom} = useStyledSafeAreaInsets(); + const formattedAddress = PersonalDetailsUtils.getFormattedAddress(privatePersonalDetails ?? {}); useEffect(() => { @@ -161,11 +164,11 @@ function ReportCardLostPage({ title={translate('reportCardLostOrDamaged.screenTitle')} onBackButtonPress={handleBackButtonPress} /> - + {isReasonConfirmed ? ( <> - {translate('reportCardLostOrDamaged.confirmAddressTitle')} + {translate('reportCardLostOrDamaged.confirmAddressTitle')} {isDamaged ? ( - {translate('reportCardLostOrDamaged.cardDamagedInfo')} + {translate('reportCardLostOrDamaged.cardDamagedInfo')} ) : ( - {translate('reportCardLostOrDamaged.cardLostOrStolenInfo')} + {translate('reportCardLostOrDamaged.cardLostOrStolenInfo')} )} ) : ( <> - + {translate('reportCardLostOrDamaged.reasonTitle')} card.nameValuePairs?.isVirtual); const virtualCardError = ErrorUtils.getLatestErrorMessage(virtualCard?.errors ?? {}); + const {paddingBottom} = useStyledSafeAreaInsets(); + const prevIsLoading = usePrevious(formData?.isLoading); useEffect(() => { @@ -70,7 +73,7 @@ function ReportVirtualCardFraudPage({ title={translate('reportFraudPage.title')} onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))} /> - + {translate('reportFraudPage.description')} diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx index 44cb485c0b06..54e629236d0a 100644 --- a/src/pages/tasks/NewTaskPage.tsx +++ b/src/pages/tasks/NewTaskPage.tsx @@ -11,6 +11,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; @@ -54,6 +55,8 @@ function NewTaskPage({task, reports, personalDetails}: NewTaskPageProps) { const isAllowedToCreateTask = useMemo(() => isEmptyObject(parentReport) || ReportUtils.isAllowedToComment(parentReport), [parentReport]); + const {paddingBottom} = useStyledSafeAreaInsets(); + useEffect(() => { setErrorMessage(''); @@ -191,7 +194,7 @@ function NewTaskPage({task, reports, personalDetails}: NewTaskPageProps) { onSubmit={onSubmit} enabledWhenOffline buttonText={translate('newTaskPage.confirmTask')} - containerStyles={[styles.mh0, styles.mt5, styles.flex1, styles.ph5]} + containerStyles={[styles.mh0, styles.mt5, styles.flex1, styles.ph5, !paddingBottom ? styles.mb5 : null]} /> From 2d472a66f0ab753adbecfd60b96f71dc2b02d2a2 Mon Sep 17 00:00:00 2001 From: Viktor Kenyz Date: Wed, 24 Apr 2024 19:12:24 +0300 Subject: [PATCH 052/219] Add tracking of closed clients --- src/libs/ActiveClientManager/index.ts | 47 +++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/libs/ActiveClientManager/index.ts b/src/libs/ActiveClientManager/index.ts index e703ce0458f4..6ab77acfed6a 100644 --- a/src/libs/ActiveClientManager/index.ts +++ b/src/libs/ActiveClientManager/index.ts @@ -7,10 +7,11 @@ import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import * as ActiveClients from '@userActions/ActiveClients'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Init, IsClientTheLeader, IsReady} from './types'; +import type { Init, IsClientTheLeader, IsReady } from './types'; const clientID = Str.guid(); const maxClients = 20; +const ACTIVE_CLIENT_LEFT_KEY = 'activeClientLeft'; let activeClients: string[] = []; let resolveSavedSelfPromise: () => void; const savedSelfPromise = new Promise((resolve) => { @@ -45,6 +46,46 @@ Onyx.connect({ }, }); +const cleanUpOnPageHide = () => { + // notify other open tabs that this client is closed + localStorage.setItem(ACTIVE_CLIENT_LEFT_KEY, clientID); +}; + +const syncLocal = ({ key, newValue: clientLeftID }: StorageEvent) => { + if (key !== ACTIVE_CLIENT_LEFT_KEY) { + return; + } + + // clean clientID of recently closed tab + // since it's not possible to write to IDB on closing stage + if (clientLeftID && activeClients.includes(clientLeftID) && clientLeftID !== clientID) { + activeClients = activeClients.filter(id => id !== clientLeftID); + ActiveClients.setActiveClients(activeClients); + } + localStorage.removeItem(ACTIVE_CLIENT_LEFT_KEY); +}; + +const setupCleanUp = () => { + const previousClientID = localStorage.getItem(ACTIVE_CLIENT_LEFT_KEY); + + // cleanup of last closed client + if (previousClientID) { + activeClients = activeClients.filter((id) => id !== previousClientID); + ActiveClients.setActiveClients(activeClients); + localStorage.removeItem(ACTIVE_CLIENT_LEFT_KEY); + } + + // use onpagehide event since onbeforeunload in not recommended. keeping beforeunload for legacy browsers + // https://developer.chrome.com/docs/web-platform/page-lifecycle-api#the_beforeunload_event + const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'beforeunload'; + + // for tracking tab close + window.addEventListener(terminationEvent, cleanUpOnPageHide); + + // listen to localStorage change + window.addEventListener('storage', syncLocal); +}; + /** * Add our client ID to the list of active IDs. * We want to ensure we have no duplicates and that the activeClient gets added at the end of the array (see isClientTheLeader) @@ -53,6 +94,8 @@ const init: Init = () => { activeClients = activeClients.filter((id) => id !== clientID); activeClients.push(clientID); ActiveClients.setActiveClients(activeClients).then(resolveSavedSelfPromise); + + setupCleanUp(); }; /** @@ -64,4 +107,4 @@ const isClientTheLeader: IsClientTheLeader = () => { return lastActiveClient === clientID; }; -export {init, isClientTheLeader, isReady}; +export { init, isClientTheLeader, isReady }; From f04eec8e95cda3ac78392fa7d078465965766f7b Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 12:47:49 +0700 Subject: [PATCH 053/219] feat create automated tests for Workspace Taxes page --- tests/actions/PolicyTaxTest.ts | 512 +++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 tests/actions/PolicyTaxTest.ts diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts new file mode 100644 index 000000000000..96ce6274ee3c --- /dev/null +++ b/tests/actions/PolicyTaxTest.ts @@ -0,0 +1,512 @@ +import Onyx from 'react-native-onyx'; +import {createPolicyTax, deletePolicyTaxes, renamePolicyTax, setPolicyTaxesEnabled, updatePolicyTaxValue} from '@libs/actions/TaxRate'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as Policy from '@src/libs/actions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy as PolicyType, TaxRate} from '@src/types/onyx'; +import createRandomPolicy from '../utils/collections/policies'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +OnyxUpdateManager(); +describe('actions/PolicyTax', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('SetPolicyCustomTaxName', () => { + it('Set policy`s custom tax name', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const customTaxName = 'Custom tag name'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(customTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.pendingFields?.name).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('SetPolicyCurrencyDefaultTax', () => { + it('Set policy`s currency default tax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxCode = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('SetPolicyForeignCurrencyDefaultTax', () => { + it('Set policy`s foreign currency default', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxCode = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.foreignTaxDefault).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if the policy pendingFields was cleared + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('CreatePolicyTax', () => { + it('Create a new tax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const newTaxRate: TaxRate = { + name: 'Tax rate 2', + value: '2%', + code: 'id_TAX_RATE_2', + }; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + createPolicyTax(fakePolicy.id, newTaxRate); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.code).toBe(newTaxRate.code); + expect(createdTax?.name).toBe(newTaxRate.name); + expect(createdTax?.value).toBe(newTaxRate.value); + expect(createdTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.errors).toBeFalsy(); + expect(createdTax?.pendingFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('SetPolicyTaxesEnabled', () => { + it('Disable policy`s taxes', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const disableTaxID = 'id_TAX_RATE_1'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + + describe('RenamePolicyTax', () => { + it('Rename tax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxID = 'id_TAX_RATE_1'; + const newTaxName = 'Tax rate 1 updated'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + renamePolicyTax(fakePolicy.id, taxID, newTaxName); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(newTaxName); + expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.name).toBeFalsy(); + expect(updatedTax?.pendingFields?.name).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('UpdatePolicyTaxValue', () => { + it('Update tax`s value', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const taxID = 'id_TAX_RATE_1'; + const newTaxValue = 10; + const stringTaxValue = `${newTaxValue}%`; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(stringTaxValue); + expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.value).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.value).toBeFalsy(); + expect(updatedTax?.pendingFields?.value).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('DeletePolicyTaxes', () => { + it('Delete tax that is not foreignTaxDefault', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('Delete tax that is foreignTaxDefault', () => { + const fakePolicy: PolicyType = { + ...createRandomPolicy(0), + taxRates: { + ...CONST.DEFAULT_TAX, + foreignTaxDefault: 'id_TAX_RATE_1', + }, + }; + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + const firstTaxID = 'id_TAX_EXEMPT'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(taxRates?.foreignTaxDefault).toBe(firstTaxID); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); +}); From 474f687a3f1634e0f4addccf897e9d1064ab54a5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 14:41:48 +0700 Subject: [PATCH 054/219] fix lint --- tests/actions/PolicyTaxTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index 96ce6274ee3c..3899e0c2a24e 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -455,7 +455,6 @@ describe('actions/PolicyTax', () => { foreignTaxDefault: 'id_TAX_RATE_1', }, }; - const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; const taxID = 'id_TAX_RATE_1'; const firstTaxID = 'id_TAX_EXEMPT'; From 93793e69f762261faf2a0901ae42c613b17b0c9f Mon Sep 17 00:00:00 2001 From: Viktor Kenyz Date: Fri, 26 Apr 2024 17:26:00 +0300 Subject: [PATCH 055/219] Fix linting --- src/libs/ActiveClientManager/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ActiveClientManager/index.ts b/src/libs/ActiveClientManager/index.ts index 6ab77acfed6a..792d02ade5d7 100644 --- a/src/libs/ActiveClientManager/index.ts +++ b/src/libs/ActiveClientManager/index.ts @@ -7,7 +7,7 @@ import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import * as ActiveClients from '@userActions/ActiveClients'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { Init, IsClientTheLeader, IsReady } from './types'; +import type {Init, IsClientTheLeader, IsReady} from './types'; const clientID = Str.guid(); const maxClients = 20; @@ -51,7 +51,7 @@ const cleanUpOnPageHide = () => { localStorage.setItem(ACTIVE_CLIENT_LEFT_KEY, clientID); }; -const syncLocal = ({ key, newValue: clientLeftID }: StorageEvent) => { +const syncLocal = ({key, newValue: clientLeftID}: StorageEvent) => { if (key !== ACTIVE_CLIENT_LEFT_KEY) { return; } @@ -59,7 +59,7 @@ const syncLocal = ({ key, newValue: clientLeftID }: StorageEvent) => { // clean clientID of recently closed tab // since it's not possible to write to IDB on closing stage if (clientLeftID && activeClients.includes(clientLeftID) && clientLeftID !== clientID) { - activeClients = activeClients.filter(id => id !== clientLeftID); + activeClients = activeClients.filter((id) => id !== clientLeftID); ActiveClients.setActiveClients(activeClients); } localStorage.removeItem(ACTIVE_CLIENT_LEFT_KEY); @@ -107,4 +107,4 @@ const isClientTheLeader: IsClientTheLeader = () => { return lastActiveClient === clientID; }; -export { init, isClientTheLeader, isReady }; +export {init, isClientTheLeader, isReady}; From 7be21e56fe3f836982d8a01e28fd99df9fdf5ae2 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 29 Apr 2024 16:15:34 +0200 Subject: [PATCH 056/219] merge STYLE and TS_STYLE, update TS_CHEATSHEET --- .../PROPTYPES_CONVERSION_TABLE.md | 144 --- contributingGuides/STYLE.md | 875 ++++++++++++++---- contributingGuides/TS_CHEATSHEET.md | 6 +- contributingGuides/TS_STYLE.md | 746 --------------- 4 files changed, 719 insertions(+), 1052 deletions(-) delete mode 100644 contributingGuides/PROPTYPES_CONVERSION_TABLE.md delete mode 100644 contributingGuides/TS_STYLE.md diff --git a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md deleted file mode 100644 index 7e1c1dda4262..000000000000 --- a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md +++ /dev/null @@ -1,144 +0,0 @@ -# Expensify PropTypes Conversion Table - -This is a reference to help you convert PropTypes to TypeScript types. - -## Table of Contents - -- [Important Considerations](#important-considerations) - - [Don't Rely on `isRequired`](#dont-rely-on-isrequired) -- [PropTypes Conversion Table](#proptypes-conversion-table) -- [Conversion Example](#conversion-example) - -## Important Considerations - -### Don't Rely on `isRequired` - -Regardless of `isRequired` is present or not on props in `PropTypes`, read through the component implementation to check if props without `isRequired` can actually be optional. The use of `isRequired` is not consistent in the current codebase. Just because `isRequired` is not present, it does not necessarily mean that the prop is optional. - -One trick is to mark the prop in question with optional modifier `?`. See if the "possibly `undefined`" error is raised by TypeScript. If any error is raised, the implementation assumes the prop not to be optional. - -```ts -// Before -const propTypes = { - isVisible: PropTypes.bool.isRequired, - // `confirmText` prop is not marked as required here, theoretically it is optional. - confirmText: PropTypes.string, -}; - -// After -type ComponentProps = { - isVisible: boolean; - // Consider it as required unless you have proof that it is indeed an optional prop. - confirmText: string; // vs. confirmText?: string; -}; -``` - -## PropTypes Conversion Table - -| PropTypes | TypeScript | Instructions | -| -------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PropTypes.any` | `T`, `Record` or `unknown` | Figure out what would be the correct data type and use it.

If you know that it's a object but isn't possible to determine the internal structure, use `Record`. | -| `PropTypes.array` or `PropTypes.arrayOf(T)` | `T[]` or `Array` | Convert to `T[]`, where `T` is the data type of the array.

If `T` isn't a primitive type, create a separate `type` for the object structure of your prop and use it. | -| `PropTypes.bool` | `boolean` | Convert to `boolean`. | -| `PropTypes.func` | `(arg1: Type1, arg2: Type2...) => ReturnType` | Convert to the function signature. | -| `PropTypes.number` | `number` | Convert to `number`. | -| `PropTypes.object`, `PropTypes.shape(T)` or `PropTypes.exact(T)` | `T` | If `T` isn't a primitive type, create a separate `type` for the `T` object structure of your prop and use it.

If you want an object but it isn't possible to determine the internal structure, use `Record`. | -| `PropTypes.objectOf(T)` | `Record` | Convert to a `Record` where `T` is the data type of values stored in the object.

If `T` isn't a primitive type, create a separate `type` for the object structure and use it. | -| `PropTypes.string` | `string` | Convert to `string`. | -| `PropTypes.node` | `React.ReactNode` | Convert to `React.ReactNode`. `ReactNode` includes `ReactElement` as well as other types such as `strings`, `numbers`, `arrays` of the same, `null`, and `undefined` In other words, anything that can be rendered in React is a `ReactNode`. | -| `PropTypes.element` | `React.ReactElement` | Convert to `React.ReactElement`. | -| `PropTypes.symbol` | `symbol` | Convert to `symbol`. | -| `PropTypes.elementType` | `React.ElementType` | Convert to `React.ElementType`. | -| `PropTypes.instanceOf(T)` | `T` | Convert to `T`. | -| `PropTypes.oneOf([T, U, ...])` or `PropTypes.oneOfType([T, U, ...])` | `T \| U \| ...` | Convert to a union type e.g. `T \| U \| ...`. | - -## Conversion Example - -```ts -// Before -const propTypes = { - unknownData: PropTypes.any, - anotherUnknownData: PropTypes.any, - indexes: PropTypes.arrayOf(PropTypes.number), - items: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string, - label: PropTypes.string, - }) - ), - shouldShowIcon: PropTypes.bool, - onChangeText: PropTypes.func, - count: PropTypes.number, - session: PropTypes.shape({ - authToken: PropTypes.string, - accountID: PropTypes.number, - }), - errors: PropTypes.objectOf(PropTypes.string), - inputs: PropTypes.objectOf( - PropTypes.shape({ - id: PropTypes.string, - label: PropTypes.string, - }) - ), - label: PropTypes.string, - anchor: PropTypes.node, - footer: PropTypes.element, - uniqSymbol: PropTypes.symbol, - icon: PropTypes.elementType, - date: PropTypes.instanceOf(Date), - size: PropTypes.oneOf(["small", "medium", "large"]), - - optionalString: PropTypes.string, - /** - * Note that all props listed above are technically optional because they lack the `isRequired` attribute. - * However, in most cases, props are actually required but the `isRequired` attribute is left out by mistake. - * - * For each prop that appears to be optional, determine whether the component implementation assumes that - * the prop has a value (making it non-optional) or not. Only those props that are truly optional should be - * labeled with a `?` in their type definition. - */ -}; - -// After -type Item = { - value: string; - label: string; -}; - -type Session = { - authToken: string; - accountID: number; -}; - -type Input = { - id: string; - label: string; -}; - -type Size = "small" | "medium" | "large"; - -type ComponentProps = { - unknownData: string[]; - - // It's not possible to infer the data as it can be anything because of reasons X, Y and Z. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - anotherUnknownData: unknown; - - indexes: number[]; - items: Item[]; - shouldShowIcon: boolean; - onChangeText: (value: string) => void; - count: number; - session: Session; - errors: Record; - inputs: Record; - label: string; - anchor: React.ReactNode; - footer: React.ReactElement; - uniqSymbol: symbol; - icon: React.ElementType; - date: Date; - size: Size; - optionalString?: string; -}; -``` diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 1e2bf7f594e8..7bd813f48aa7 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -1,5 +1,6 @@ -# JavaScript Coding Standards +# Coding Standards + For almost all of our code style rules, refer to the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). When writing ES6 or React code, please also refer to the [Airbnb React/JSX Style Guide](https://github.com/airbnb/javascript/tree/master/react). @@ -10,8 +11,509 @@ We use Prettier to automatically style our code. There are a few things that we have customized for our tastes which will take precedence over Airbnb's guide. +## TypeScript guidelines + +### General Rules + +Strive to type as strictly as possible. + +```ts +type Foo = { + fetchingStatus: "loading" | "success" | "error"; // vs. fetchingStatus: string; + person: { name: string; age: number }; // vs. person: Record; +}; +``` + +### `d.ts` Extension + +Do not use `d.ts` file extension even when a file contains only type declarations. Only exceptions are `src/types/global.d.ts` and `src/types/modules/*.d.ts` files in which third party packages and JavaScript's built-in modules (e.g. `window` object) can be modified using module augmentation. Refer to the [Communication Items](#communication-items) section to learn more about module augmentation. + +> Why? Type errors in `d.ts` files are not checked by TypeScript. + +### Type Alias vs. Interface + +Do not use `interface`. Use `type`. eslint: [`@typescript-eslint/consistent-type-definitions`](https://typescript-eslint.io/rules/consistent-type-definitions/) + +> Why? In TypeScript, `type` and `interface` can be used interchangeably to declare types. Use `type` for consistency. + +```ts +// BAD +interface Person { + name: string; +} + +// GOOD +type Person = { + name: string; +}; +``` + +### Enum vs. Union Type + +Do not use `enum`. Use union types. eslint: [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) + +> Why? Enums come with several [pitfalls](https://blog.logrocket.com/why-typescript-enums-suck/). Most enum use cases can be replaced with union types. + +```ts +// Most simple form of union type. +type Color = "red" | "green" | "blue"; +function printColors(color: Color) { + console.log(color); +} + +// When the values need to be iterated upon. +import { TupleToUnion } from "type-fest"; + +const COLORS = ["red", "green", "blue"] as const; +type Color = TupleToUnion; // type: 'red' | 'green' | 'blue' + +for (const color of COLORS) { + printColor(color); +} + +// When the values should be accessed through object keys. (i.e. `COLORS.Red` vs. `"red"`) +import { ValueOf } from "type-fest"; + +const COLORS = { + Red: "red", + Green: "green", + Blue: "blue", +} as const; +type Color = ValueOf; // type: 'red' | 'green' | 'blue' + +printColor(COLORS.Red); +``` + +### `unknown` vs. `any` + +Don't use `any`. Use `unknown` if type is not known beforehand. eslint: [`@typescript-eslint/no-explicit-any`](https://typescript-eslint.io/rules/no-explicit-any/) + +> Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. + +```ts +const value: unknown = JSON.parse(someJson); +if (typeof value === 'string') {...} +else if (isPerson(value)) {...} +... +``` + +### `T[]` vs. `Array` + +Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). eslint: [`@typescript-eslint/array-type`](https://typescript-eslint.io/rules/array-type/) + +```ts +// Array +const a: Array = ["a", "b"]; +const b: Array<{ prop: string }> = [{ prop: "a" }]; +const c: Array<() => void> = [() => {}]; + +// T[] +const d: MyType[] = ["a", "b"]; +const e: string[] = ["a", "b"]; +const f: readonly string[] = ["a", "b"]; +``` + +### @ts-ignore + +Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. + +### Type Inference + +When possible, allow the compiler to infer type of variables. + +```ts +// BAD +const foo: string = "foo"; +const [counter, setCounter] = useState(0); + +// GOOD +const foo = "foo"; +const [counter, setCounter] = useState(0); +const [username, setUsername] = useState(undefined); // Username is a union type of string and undefined, and its type cannot be inferred from the default value of undefined +``` + +For function return types, default to always typing them unless a function is simple enough to reason about its return type. + +> Why? Explicit return type helps catch errors when implementation of the function changes. It also makes it easy to read code even when TypeScript intellisense is not provided. + +```ts +function simpleFunction(name: string) { + return `hello, ${name}`; +} + +function complicatedFunction(name: string): boolean { +// ... some complex logic here ... + return foo; +} +``` + +### Utility Types + +Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. + +```ts +type Foo = { + bar: string; +}; + +// BAD +type ReadOnlyFoo = { + readonly [Property in keyof Foo]: Foo[Property]; +}; + +// GOOD +type ReadOnlyFoo = Readonly; +``` + +### `object` type + +Don't use `object` type. eslint: [`@typescript-eslint/ban-types`](https://typescript-eslint.io/rules/ban-types/) + +> Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. + +```ts +// BAD +const foo: object = [1, 2, 3]; // TypeScript does not error +``` + +If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. + +> Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. + +```ts +function logObject(object: Record) { + for (const [key, value] of Object.entries(object)) { + console.log(`${key}: ${value}`); + } +} +``` + +### Prop Types + +Don't use `ComponentProps` to grab a component's prop types. Go to the source file for the component and export prop types from there. Import and use the exported prop types. + +> Don't export prop types from component files by default. Only export it when there is a code that needs to access the prop type directly. + +```tsx +// MyComponent.tsx +export type MyComponentProps = { + foo: string; +}; + +export default function MyComponent({ foo }: MyComponentProps) { + return {foo}; +} + +// BAD +import { ComponentProps } from "React"; +import MyComponent from "./MyComponent"; +type MyComponentProps = ComponentProps; + +// GOOD +import MyComponent, { MyComponentProps } from "./MyComponent"; +``` + +### File organization + +In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. Do not use [`satisfies` operator](#satisfies-operator) for platform-specific implementations, always define shared types that complies with all variants. + +> Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implement (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information. + +Utility module example + +```ts +// types.ts +type GreetingModule = { + getHello: () => string; + getGoodbye: () => string; +}; + +// index.native.ts +import { GreetingModule } from "./types"; +function getHello() { + return "hello from mobile code"; +} +function getGoodbye() { + return "goodbye from mobile code"; +} +const Greeting: GreetingModule = { + getHello, + getGoodbye, +}; +export default Greeting; + +// index.ts +import { GreetingModule } from "./types"; +function getHello() { + return "hello from other platform code"; +} +function getGoodbye() { + return "goodbye from other platform code"; +} +const Greeting: GreetingModule = { + getHello, + getGoodbye, +}; +export default Greeting; +``` + +Component module example + +```ts +// types.ts +export type MyComponentProps = { + foo: string; +} + +// index.ios.ts +import { MyComponentProps } from "./types"; + +export MyComponentProps; +export default function MyComponent({ foo }: MyComponentProps) { /* ios specific implementation */ } + +// index.ts +import { MyComponentProps } from "./types"; + +export MyComponentProps; +export default function MyComponent({ foo }: MyComponentProps) { /* Default implementation */ } +``` + +### Reusable Types + +Reusable type definitions, such as models (e.g., Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. + +```ts +// src/types/Report.ts + +type Report = {...}; + +export default Report; +``` + +### tsx + +Use `.tsx` extension for files that contain React syntax. + +> Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. + +### No inline prop types + +Do not define prop types inline for components that are exported. + +> Why? Prop types might [need to be exported from component files](#export-prop-types). If the component is only used inside a file or module and not exported, then inline prop types can be used. + +```ts +// BAD +export default function MyComponent({ foo, bar }: { foo: string, bar: number }){ + // component implementation +}; + +// GOOD +type MyComponentProps = { foo: string, bar: number }; +export default MyComponent({ foo, bar }: MyComponentProps){ + // component implementation +} +``` + +### Satisfies Operator + +Use the `satisfies` operator when you need to validate that the structure of an expression matches a specific type, without affecting the resulting type of the expression. + +> Why? TypeScript developers often want to ensure that an expression aligns with a certain type, but they also want to retain the most specific type possible for inference purposes. The `satisfies` operator assists in doing both. + +```ts +// BAD +const sizingStyles = { + w50: { + width: '50%', + }, + mw100: { + maxWidth: '100%', + }, +} as const; + +// GOOD +const sizingStyles = { + w50: { + width: '50%', + }, + mw100: { + maxWidth: '100%', + }, +} satisfies Record; +``` + +### Type imports/exports + +Always use the `type` keyword when importing/exporting types + +> Why? In order to improve code clarity and consistency and reduce bundle size after typescript transpilation, we enforce the all type imports/exports to contain the `type` keyword. This way, TypeScript can automatically remove those imports from the transpiled JavaScript bundle + +Imports: +```ts +// BAD +import {SomeType} from './a' +import someVariable from './a' + +import {someVariable, SomeOtherType} from './b' + +// GOOD +import type {SomeType} from './a' +import someVariable from './a' +``` + + Exports: +```ts +// BAD +export {SomeType} +export someVariable +// or +export {someVariable, SomeOtherType} + +// GOOD +export type {SomeType} +export someVariable +``` + +### Exception to Rules + +Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily. + +When an exception is granted, link the relevant Slack conversation in your PR. Suppress ESLint or TypeScript warnings/errors with comments if necessary. + +This rule will apply until the migration is done. After the migration, discussion on granting exception can happen inside the PR page and doesn't need take place in the Slack channel. + +### Communication Items + +> Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. + +- I think types definitions in a third party library or JavaScript's built-in module are incomplete or incorrect + +When the library indeed contains incorrect or missing type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/types/modules/*.d.ts`, each library as a separate file. + +```ts +// external-library-name.d.ts + +declare module "external-library-name" { + interface LibraryComponentProps { + // Add or modify typings + additionalProp: string; + } +} +``` + +### Other Expensify Resources on TypeScript + +- [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) + ## Naming Conventions +### Types + + - Use PascalCase for type names. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) + + ```ts + // BAD + type foo = ...; + type BAR = ...; + + // GOOD + type Foo = ...; + type Bar = ...; + ``` + + - Do not postfix type aliases with `Type`. + + ```ts + // BAD + type PersonType = ...; + + // GOOD + type Person = ...; + ``` + + - Use singular name for union types. + + ```ts + // BAD + type Colors = "red" | "blue" | "green"; + + // GOOD + type Color = "red" | "blue" | "green"; + ``` + + - Use `{ComponentName}Props` pattern for prop types. + + ```ts + // BAD + type Props = { + // component's props + }; + + function MyComponent({}: Props) { + // component's code + } + + // GOOD + type MyComponentProps = { + // component's props + }; + + function MyComponent({}: MyComponentProps) { + // component's code + } + ``` + + - Use {ComponentName}Handle for custon ref handle types. + + ```tsx + // BAD + type MyComponentRef = { + onPressed: () => void; + }; + + // GOOD + type MyComponentHandle = { + onPressed: () => void; + };s + ``` + + - For generic type parameters, use `T` if you have only one type parameter. Don't use the `T`, `U`, `V`... sequence. Make type parameter names descriptive, each prefixed with `T`. + + > Prefix each type parameter name to distinguish them from other types. + + ```ts + // BAD + type KeyValuePair = { key: K; value: U }; + + type Keys = Array; + + // GOOD + type KeyValuePair = { key: TKey; value: TValue }; + + type Keys = Array; + type Keys = Array; + ``` + +### Prop callbacks + - Prop callbacks should be named for what has happened, not for what is going to happen. Components should never assume anything about how they will be used (that's the job of whatever is implementing it). + + ```ts + // Bad + type ComponentProps = { + /** A callback to call when we want to save the form */ + onSaveForm: () => void; + }; + + // Good + type ComponentProps = { + /** A callback to call when the form has been submitted */ + onFormSubmitted: () => void; + }; + ``` + + * Do not use underscores when naming private methods. + ### Event Handlers - When you have an event handler, do not prefix it with "on" or "handle". The method should be named for what it does, not what it handles. This promotes code reuse by minimizing assumptions that a method must be called in a certain fashion (eg. only as an event handler). - One exception for allowing the prefix of "on" is when it is used for callback `props` of a React component. Using it in this way helps to distinguish callbacks from public component methods. @@ -78,7 +580,7 @@ someArray.map(function (item) {...}); someArray.map((item) => {...}); ``` -Empty functions (noop) should be declare as arrow functions with no whitespace inside. Avoid _.noop() +Empty functions (noop) should be declared as arrow functions with no whitespace inside. Avoid _.noop() ```javascript // Bad @@ -114,53 +616,68 @@ if (someCondition) { ## Accessing Object Properties and Default Values -Use `lodashGet()` to safely access object properties and `||` to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. In the rare case that you want to consider a falsy value as usable and the `||` operator prevents this then be explicit about this in your code and check for the type. +Use optional chaining (`?.`) to safely access object properties and nullish coalescing (`??`) to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. Don't use the `lodashGet()` function. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) -```javascript -// Bad -const value = somePossiblyNullThing ?? 'default'; -// Good -const value = somePossiblyNullThing || 'default'; -// Bad -const value = someObject.possiblyUndefinedProperty?.nestedProperty || 'default'; -// Bad -const value = (someObject && someObject.possiblyUndefinedProperty && someObject.possiblyUndefinedProperty.nestedProperty) || 'default'; -// Good -const value = lodashGet(someObject, 'possiblyUndefinedProperty.nestedProperty', 'default'); +```ts +// BAD +import lodashGet from "lodash/get"; +const name = lodashGet(user, "name", "default name"); + +// BAD +const name = user?.name || "default name"; + +// GOOD +const name = user?.name ?? "default name"; ``` ## JSDocs -- Always document parameters and return values. -- Optional parameters should be enclosed by `[]` e.g. `@param {String} [optionalText]`. -- Document object parameters with separate lines e.g. `@param {Object} parameters` followed by `@param {String} parameters.field`. -- If a parameter accepts more than one type use `*` to denote there is no single type. -- Use uppercase when referring to JS primitive values (e.g. `Boolean` not `bool`, `Number` not `int`, etc). -- When specifying a return value use `@returns` instead of `@return`. If there is no return value do not include one in the doc. - +- Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override`. eslint: [`jsdoc/no-types`](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.README/rules/no-types.md) +- Only document params/return values if their names are not enough to fully understand their purpose. Not all parameters or return values need to be listed in the JSDoc comment. If there is no comment accompanying the parameter or return value, omit it. +- When specifying a return value use `@returns` instead of `@return`. - Avoid descriptions that don't add any additional information. Method descriptions should only be added when it's behavior is unclear. - Do not use block tags other than `@param` and `@returns` (e.g. `@memberof`, `@constructor`, etc). - Do not document default parameters. They are already documented by adding them to a declared function's arguments. - Do not use record types e.g. `{Object.}`. -- Do not create `@typedef` to use in JSDocs. -- Do not use type unions e.g. `{(number|boolean)}`. -```javascript -// Bad +```ts +// BAD /** - * Populates the shortcut modal - * @param {bool} shouldShowAdvancedShortcuts whether to show advanced shortcuts - * @return {*} + * @param {number} age + * @returns {boolean} Whether the person is a legal drinking age or nots */ -function populateShortcutModal(shouldShowAdvancedShortcuts) { +function canDrink(age: number): boolean { + return age >= 21; } -// Good +// GOOD /** - * @param {Boolean} shouldShowAdvancedShortcuts - * @returns {Boolean} + * @returns Whether the person is a legal drinking age or nots */ -function populateShortcutModal(shouldShowAdvancedShortcuts) { +function canDrink(age: number): boolean { + return age >= 21; +} +``` + +In the above example, because the parameter `age` doesn't have any accompanying comment, it is completely omitted from the JSDoc. + +## Component props + +Do not use **`propTypes` and `defaultProps`**: . Use object destructing and assign a default value to each optional prop unless the default values is `undefined`. + +```tsx +type MyComponentProps = { + requiredProp: string; + optionalPropWithDefaultValue?: number; + optionalProp?: boolean; +}; + +function MyComponent({ + requiredProp, + optionalPropWithDefaultValue = 42, + optionalProp, +}: MyComponentProps) { + // component's code } ``` @@ -179,35 +696,6 @@ const {data} = event.data; const {name, accountID, email} = data; ``` -**React Components** - -Always use destructuring to get prop values. Destructuring is necessary to assign default values to props. - -```javascript -// Bad -function UserInfo(props) { - return ( - - Name: {props.name} - Email: {props.email} - -} - -UserInfo.defaultProps = { - name: 'anonymous'; -} - -// Good -function UserInfo({ name = 'anonymous', email }) { - return ( - - Name: {name} - Email: {email} - - ); -} -``` - ## Named vs Default Exports in ES6 - When to use what? ES6 provides two ways to export a module from a file: `named export` and `default export`. Which variation to use depends on how the module will be used. @@ -277,98 +765,47 @@ So, if a new language feature isn't something we have agreed to support it's off Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase: - **Async/Await** - Use the native `Promise` instead -- **Optional Chaining** - Use `lodashGet()` to fetch a nested value instead -- **Null Coalescing Operator** - Use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable +- **Optional Chaining** - Yes, don't use `lodashGet()` +- **Null Coalescing Operator** - Yes, don't use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable # React Coding Standards # React specific styles -## Method Naming and Code Documentation -* Prop callbacks should be named for what has happened, not for what is going to happen. Components should never assume anything about how they will be used (that's the job of whatever is implementing it). - -```javascript -// Bad -const propTypes = { - /** A callback to call when we want to save the form */ - onSaveForm: PropTypes.func.isRequired, -}; - -// Good -const propTypes = { - /** A callback to call when the form has been submitted */ - onFormSubmitted: PropTypes.func.isRequired, -}; -``` +## Code Documentation -* Do not use underscores when naming private methods. -* Add descriptions to all `propTypes` using a block comment above the definition. No need to document the types (that's what `propTypes` is doing already), but add some context for each property so that other developers understand the intended use. +* Add descriptions to all component props using a block comment above the definition. No need to document the types, but add some context for each property so that other developers understand the intended use. ```javascript // Bad -const propTypes = { - currency: PropTypes.string.isRequired, - amount: PropTypes.number.isRequired, - isIgnored: PropTypes.bool.isRequired +type ComponentProps = { + currency: string; + amount: number; + isIgnored: boolean; }; // Bad -const propTypes = { +type ComponentProps = { // The currency that the reward is in - currency: React.PropTypes.string.isRequired, + currency: string; // The amount of reward - amount: React.PropTypes.number.isRequired, + amount: number; // If the reward has been ignored or not - isIgnored: React.PropTypes.bool.isRequired + isIgnored: boolean; } // Good -const propTypes = { +type ComponentProps = { /** The currency that the reward is in */ - currency: React.PropTypes.string.isRequired, + currency: string; /** The amount of the reward */ - amount: React.PropTypes.number.isRequired, + amount: number; /** If the reward has not been ignored yet */ - isIgnored: React.PropTypes.bool.isRequired -} -``` - -All `propTypes` and `defaultProps` *must* be defined at the **top** of the file in variables called `propTypes` and `defaultProps`. -These variables should then be assigned to the component at the bottom of the file. - -```js -MyComponent.propTypes = propTypes; -MyComponent.defaultProps = defaultProps; -export default MyComponent; -``` - -Any nested `propTypes` e.g. that may appear in a `PropTypes.shape({})` should also be documented. - -```javascript -// Bad -const propTypes = { - /** Session data */ - session: PropTypes.shape({ - authToken: PropTypes.string, - login: PropTypes.string, - }), -} - -// Good -const propTypes = { - /** Session data */ - session: PropTypes.shape({ - - /** Token used to authenticate the user */ - authToken: PropTypes.string, - - /** User email or phone number */ - login: PropTypes.string, - }), + isIgnored: boolean; } ``` @@ -452,61 +889,163 @@ In React Native, one **must not** attempt to falsey-check a string for an inline When writing a function component you must ALWAYS add a `displayName` property and give it the same value as the name of the component (this is so it appears properly in the React dev tools) ```javascript +function Avatar(props: AvatarProps) {...}; - function Avatar(props) {...}; - - Avatar.propTypes = propTypes; - Avatar.defaultProps = defaultProps; - Avatar.displayName = 'Avatar'; +Avatar.displayName = 'Avatar'; - export default Avatar; +export default Avatar; ``` ## Forwarding refs When forwarding a ref define named component and pass it directly to the `forwardRef`. By doing this we remove potential extra layer in React tree in form of anonymous component. -```javascript - function FancyInput(props, ref) { - ... - return - } +```tsx +import type {ForwarderRef} from 'react'; + +type FancyInputProps = { + ... +}; - export default React.forwardRef(FancyInput) +function FancyInput(props: FancyInputProps, ref: ForwardedRef) { + ... + return +}; + +export default React.forwardRef(FancyInput) ``` -## Stateless components vs Pure Components vs Class based components vs Render Props - When to use what? +If the ref handle is not available (e.g. `useImperativeHandle` is used) you can define a custom handle type above the component. -Class components are DEPRECATED. Use function components and React hooks. +```tsx +import type {ForwarderRef} from 'react'; +import {useImperativeHandle} from 'react'; -[https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function](https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function) +type FancyInputProps = { + ... + onButtonPressed: () => void; +}; -## Composition vs Inheritance +type FancyInputHandle = { + onButtonPressed: () => void; +} -From React's documentation - ->Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions. ->If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it. +function FancyInput(props: FancyInputProps, ref: ForwardedRef) { + useImperativeHandle(ref, () => ({onButtonPressed})); -Use an HOC a.k.a. *[Higher order component](https://reactjs.org/docs/higher-order-components.html)* if you find a use case where you need inheritance. + ... + return ; +}; -If several HOC need to be combined there is a `compose()` utility. But we should not use this utility when there is only one HOC. +export default React.forwardRef(FancyInput) +``` -```javascript -// Bad -export default compose( - withLocalize, -)(MyComponent); +## Hooks and HOCs + +Use hooks whenever possible, avoid using HOCs. + +> Why? Hooks are easier to use (can be used inside the function component), and don't need nesting or `compose` when exporting the component. It also allows us to remove `compose` completely in some components since it has been bringing up some issues with TypeScript. Read the [`compose` usage](#compose-usage) section for further information about the TypeScript issues with `compose`. + +> Note: Because Onyx doesn't provide a hook yet, in a component that accesses Onyx data with `withOnyx` HOC, please make sure that you don't use other HOCs (if applicable) to avoid HOC nesting. + +```tsx +// BAD +type ComponentOnyxProps = { + session: OnyxEntry; +}; + +type ComponentProps = WindowDimensionsProps & + WithLocalizeProps & + ComponentOnyxProps & { + someProp: string; + }; + +function Component({windowWidth, windowHeight, translate, session, someProp}: ComponentProps) { + // component's code +} -// Good export default compose( - withLocalize, withWindowDimensions, -)(MyComponent); + withLocalize, + withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + }), +)(Component); -// Good -export default withLocalize(MyComponent) +// GOOD +type ComponentOnyxProps = { + session: OnyxEntry; +}; + +type ComponentProps = ComponentOnyxProps & { + someProp: string; +}; + +function Component({session, someProp}: ComponentProps) { + const {windowWidth, windowHeight} = useWindowDimensions(); + const {translate} = useLocalize(); + // component's code +} + +// There is no hook alternative for withOnyx yet. +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(Component); ``` +## Stateless components vs Pure Components vs Class based components vs Render Props - When to use what? + +Class components are DEPRECATED. Use function components and React hooks. + +[https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function](https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function) + +## Composition + +Avoid the usage of `compose` function to compose HOCs in TypeScript files. Use nesting instead. + + > Why? `compose` function doesn't work well with TypeScript when dealing with several HOCs being used in a component, many times resulting in wrong types and errors. Instead, nesting can be used to allow a seamless use of multiple HOCs and result in a correct return type of the compoment. Also, you can use [hooks instead of HOCs](#hooks-instead-of-hocs) whenever possible to minimize or even remove the need of HOCs in the component. + +From React's documentation - +>Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions. +>If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it. + + ```ts + // BAD + export default compose( + withCurrentUserPersonalDetails, + withReportOrNotFound(), + withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + }), + )(Component); + + // GOOD + export default withCurrentUserPersonalDetails( + withReportOrNotFound()( + withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + })(Component), + ), + ); + + // GOOD - alternative to HOC nesting + const ComponentWithOnyx = withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + })(Component); + const ComponentWithReportOrNotFound = withReportOrNotFound()(ComponentWithOnyx); + export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound); + ``` + **Note:** If you find that none of these approaches work for you, please ask an Expensify engineer for guidance via Slack or GitHub. ## Use Refs Appropriately @@ -586,3 +1125,19 @@ For example, if you are storing a boolean value that could be associated with a **Exception:** There are some [gotchas](https://github.com/expensify/react-native-onyx#merging-data) when working with complex nested array values in Onyx. So, this could be another valid reason to break a property off of it's parent object (e.g. `reportActions` are easier to work with as a separate collection). If you're not sure whether something should have a collection key reach out in [`#expensify-open-source`](https://expensify.slack.com/archives/C01GTK53T8Q) for additional feedback. + +# Learning Resources + +### Quickest way to learn TypeScript + +- Get up to speed quickly + - [TypeScript playground](https://www.typescriptlang.org/play?q=231#example) + - Go though all examples on the playground. Click on "Example" tab on the top +- Handy Reference + - [TypeScript CheatSheet](https://www.typescriptlang.org/cheatsheets) + - [Type](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) + - [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) +- TypeScript with React + - [React TypeScript CheatSheet](https://react-typescript-cheatsheet.netlify.app/) + - [List of built-in utility types](https://react-typescript-cheatsheet.netlify.app/docs/basic/troubleshooting/utilities) + - [HOC CheatSheet](https://react-typescript-cheatsheet.netlify.app/docs/hoc/) \ No newline at end of file diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md index 1e330dafb7cf..77d316bb861d 100644 --- a/contributingGuides/TS_CHEATSHEET.md +++ b/contributingGuides/TS_CHEATSHEET.md @@ -21,8 +21,10 @@ - [1.1](#children-prop) **`props.children`** ```tsx - type WrapperComponentProps = { - children?: React.ReactNode; + import type ChildrenProps from '@src/types/utils/ChildrenProps'; + + type WrapperComponentProps = ChildrenProps & { + ... }; function WrapperComponent({ children }: WrapperComponentProps) { diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md deleted file mode 100644 index b96f24a7c949..000000000000 --- a/contributingGuides/TS_STYLE.md +++ /dev/null @@ -1,746 +0,0 @@ -# Expensify TypeScript Style Guide - -## Table of Contents - -- [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) -- [General Rules](#general-rules) -- [Guidelines](#guidelines) - - [1.1 Naming Conventions](#naming-conventions) - - [1.2 `d.ts` Extension](#d-ts-extension) - - [1.3 Type Alias vs. Interface](#type-alias-vs-interface) - - [1.4 Enum vs. Union Type](#enum-vs-union-type) - - [1.5 `unknown` vs. `any`](#unknown-vs-any) - - [1.6 `T[]` vs. `Array`](#array) - - [1.7 @ts-ignore](#ts-ignore) - - [1.8 Optional chaining and nullish coalescing](#ts-nullish-coalescing) - - [1.9 Type Inference](#type-inference) - - [1.10 JSDoc](#jsdoc) - - [1.11 `propTypes` and `defaultProps`](#proptypes-and-defaultprops) - - [1.12 Utility Types](#utility-types) - - [1.13 `object` Type](#object-type) - - [1.14 Export Prop Types](#export-prop-types) - - [1.15 File Organization](#file-organization) - - [1.16 Reusable Types](#reusable-types) - - [1.17 `.tsx`](#tsx) - - [1.18 No inline prop types](#no-inline-prop-types) - - [1.19 Satisfies operator](#satisfies-operator) - - [1.20 Hooks instead of HOCs](#hooks-instead-of-hocs) - - [1.21 `compose` usage](#compose-usage) - - [1.22 Type imports](#type-imports) -- [Exception to Rules](#exception-to-rules) -- [Communication Items](#communication-items) -- [Migration Guidelines](#migration-guidelines) -- [Learning Resources](#learning-resources) - -## Other Expensify Resources on TypeScript - -- [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) -- [Expensify TypeScript PropTypes Conversion Table](./PROPTYPES_CONVERSION_TABLE.md) - -## General Rules - -Strive to type as strictly as possible. - -```ts -type Foo = { - fetchingStatus: "loading" | "success" | "error"; // vs. fetchingStatus: string; - person: { name: string; age: number }; // vs. person: Record; -}; -``` - -## Guidelines - - - -- [1.1](#naming-conventions) **Naming Conventions**: Follow naming conventions specified below - - - Use PascalCase for type names. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) - - ```ts - // BAD - type foo = ...; - type BAR = ...; - - // GOOD - type Foo = ...; - type Bar = ...; - ``` - - - Do not postfix type aliases with `Type`. - - ```ts - // BAD - type PersonType = ...; - - // GOOD - type Person = ...; - ``` - - - Use singular name for union types. - - ```ts - // BAD - type Colors = "red" | "blue" | "green"; - - // GOOD - type Color = "red" | "blue" | "green"; - ``` - - - Use `{ComponentName}Props` pattern for prop types. - - ```ts - // BAD - type Props = { - // component's props - }; - - function MyComponent({}: Props) { - // component's code - } - - // GOOD - type MyComponentProps = { - // component's props - }; - - function MyComponent({}: MyComponentProps) { - // component's code - } - ``` - - - For generic type parameters, use `T` if you have only one type parameter. Don't use the `T`, `U`, `V`... sequence. Make type parameter names descriptive, each prefixed with `T`. - - > Prefix each type parameter name to distinguish them from other types. - - ```ts - // BAD - type KeyValuePair = { key: K; value: U }; - - type Keys = Array; - - // GOOD - type KeyValuePair = { key: TKey; value: TValue }; - - type Keys = Array; - type Keys = Array; - ``` - - - -- [1.2](#d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. Only exceptions are `src/types/global.d.ts` and `src/types/modules/*.d.ts` files in which third party packages and JavaScript's built-in modules (e.g. `window` object) can be modified using module augmentation. Refer to the [Communication Items](#communication-items) section to learn more about module augmentation. - - > Why? Type errors in `d.ts` files are not checked by TypeScript [^1]. - -[^1]: This is because `skipLibCheck` TypeScript configuration is set to `true` in this project. - - - -- [1.3](#type-alias-vs-interface) **Type Alias vs. Interface**: Do not use `interface`. Use `type`. eslint: [`@typescript-eslint/consistent-type-definitions`](https://typescript-eslint.io/rules/consistent-type-definitions/) - - > Why? In TypeScript, `type` and `interface` can be used interchangeably to declare types. Use `type` for consistency. - - ```ts - // BAD - interface Person { - name: string; - } - - // GOOD - type Person = { - name: string; - }; - ``` - - - -- [1.4](#enum-vs-union-type) **Enum vs. Union Type**: Do not use `enum`. Use union types. eslint: [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) - - > Why? Enums come with several [pitfalls](https://blog.logrocket.com/why-typescript-enums-suck/). Most enum use cases can be replaced with union types. - - ```ts - // Most simple form of union type. - type Color = "red" | "green" | "blue"; - function printColors(color: Color) { - console.log(color); - } - - // When the values need to be iterated upon. - import { TupleToUnion } from "type-fest"; - - const COLORS = ["red", "green", "blue"] as const; - type Color = TupleToUnion; // type: 'red' | 'green' | 'blue' - - for (const color of COLORS) { - printColor(color); - } - - // When the values should be accessed through object keys. (i.e. `COLORS.Red` vs. `"red"`) - import { ValueOf } from "type-fest"; - - const COLORS = { - Red: "red", - Green: "green", - Blue: "blue", - } as const; - type Color = ValueOf; // type: 'red' | 'green' | 'blue' - - printColor(COLORS.Red); - ``` - - - -- [1.5](#unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand. eslint: [`@typescript-eslint/no-explicit-any`](https://typescript-eslint.io/rules/no-explicit-any/) - - > Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. - - ```ts - const value: unknown = JSON.parse(someJson); - if (typeof value === 'string') {...} - else if (isPerson(value)) {...} - ... - ``` - - - -- [1.6](#array) **`T[]` vs. `Array`**: Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). eslint: [`@typescript-eslint/array-type`](https://typescript-eslint.io/rules/array-type/) - - ```ts - // Array - const a: Array = ["a", "b"]; - const b: Array<{ prop: string }> = [{ prop: "a" }]; - const c: Array<() => void> = [() => {}]; - - // T[] - const d: MyType[] = ["a", "b"]; - const e: string[] = ["a", "b"]; - const f: readonly string[] = ["a", "b"]; - ``` - - - -- [1.7](#ts-ignore) **@ts-ignore**: Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. - - > Use `@ts-expect-error` during the migration for type errors that should be handled later. Refer to the [Migration Guidelines](#migration-guidelines) for specific instructions on how to deal with type errors during the migration. eslint: [`@typescript-eslint/ban-ts-comment`](https://typescript-eslint.io/rules/ban-ts-comment/) - - - -- [1.8](#ts-nullish-coalescing) **Optional chaining and nullish coalescing**: Use optional chaining and nullish coalescing instead of the `get` lodash function. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) - - ```ts - // BAD - import lodashGet from "lodash/get"; - const name = lodashGet(user, "name", "default name"); - - // GOOD - const name = user?.name ?? "default name"; - ``` - - - -- [1.9](#type-inference) **Type Inference**: When possible, allow the compiler to infer type of variables. - - ```ts - // BAD - const foo: string = "foo"; - const [counter, setCounter] = useState(0); - - // GOOD - const foo = "foo"; - const [counter, setCounter] = useState(0); - const [username, setUsername] = useState(undefined); // Username is a union type of string and undefined, and its type cannot be inferred from the default value of undefined - ``` - - For function return types, default to always typing them unless a function is simple enough to reason about its return type. - - > Why? Explicit return type helps catch errors when implementation of the function changes. It also makes it easy to read code even when TypeScript intellisense is not provided. - - ```ts - function simpleFunction(name: string) { - return `hello, ${name}`; - } - - function complicatedFunction(name: string): boolean { - // ... some complex logic here ... - return foo; - } - ``` - - - -- [1.10](#jsdoc) **JSDoc**: Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override`. eslint: [`jsdoc/no-types`](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.README/rules/no-types.md) - - > Not all parameters or return values need to be listed in the JSDoc comment. If there is no comment accompanying the parameter or return value, omit it. - - ```ts - // BAD - /** - * @param {number} age - * @returns {boolean} Whether the person is a legal drinking age or nots - */ - function canDrink(age: number): boolean { - return age >= 21; - } - - // GOOD - /** - * @returns Whether the person is a legal drinking age or nots - */ - function canDrink(age: number): boolean { - return age >= 21; - } - ``` - - In the above example, because the parameter `age` doesn't have any accompanying comment, it is completely omitted from the JSDoc. - - - -- [1.11](#proptypes-and-defaultprops) **`propTypes` and `defaultProps`**: Do not use them. Use object destructing to assign default values if necessary. - - > Refer to [the propTypes Migration Table](./PROPTYPES_CONVERSION_TABLE.md) on how to type props based on existing `propTypes`. - - > Assign a default value to each optional prop unless the default values is `undefined`. - - ```tsx - type MyComponentProps = { - requiredProp: string; - optionalPropWithDefaultValue?: number; - optionalProp?: boolean; - }; - - function MyComponent({ - requiredProp, - optionalPropWithDefaultValue = 42, - optionalProp, - }: MyComponentProps) { - // component's code - } - ``` - - - -- [1.12](#utility-types) **Utility Types**: Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. - - ```ts - type Foo = { - bar: string; - }; - - // BAD - type ReadOnlyFoo = { - readonly [Property in keyof Foo]: Foo[Property]; - }; - - // GOOD - type ReadOnlyFoo = Readonly; - ``` - - - -- [1.13](#object-type) **`object`**: Don't use `object` type. eslint: [`@typescript-eslint/ban-types`](https://typescript-eslint.io/rules/ban-types/) - - > Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. - - ```ts - // BAD - const foo: object = [1, 2, 3]; // TypeScript does not error - ``` - - If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. - - > Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. - - ```ts - function logObject(object: Record) { - for (const [key, value] of Object.entries(object)) { - console.log(`${key}: ${value}`); - } - } - ``` - - - -- [1.14](#export-prop-types) **Prop Types**: Don't use `ComponentProps` to grab a component's prop types. Go to the source file for the component and export prop types from there. Import and use the exported prop types. - - > Don't export prop types from component files by default. Only export it when there is a code that needs to access the prop type directly. - - ```tsx - // MyComponent.tsx - export type MyComponentProps = { - foo: string; - }; - - export default function MyComponent({ foo }: MyComponentProps) { - return {foo}; - } - - // BAD - import { ComponentProps } from "React"; - import MyComponent from "./MyComponent"; - type MyComponentProps = ComponentProps; - - // GOOD - import MyComponent, { MyComponentProps } from "./MyComponent"; - ``` - - - -- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. Do not use [`satisfies` operator](#satisfies-operator) for platform-specific implementations, always define shared types that complies with all variants. - - > Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implement (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information. - - Utility module example - - ```ts - // types.ts - type GreetingModule = { - getHello: () => string; - getGoodbye: () => string; - }; - - // index.native.ts - import { GreetingModule } from "./types"; - function getHello() { - return "hello from mobile code"; - } - function getGoodbye() { - return "goodbye from mobile code"; - } - const Greeting: GreetingModule = { - getHello, - getGoodbye, - }; - export default Greeting; - - // index.ts - import { GreetingModule } from "./types"; - function getHello() { - return "hello from other platform code"; - } - function getGoodbye() { - return "goodbye from other platform code"; - } - const Greeting: GreetingModule = { - getHello, - getGoodbye, - }; - export default Greeting; - ``` - - Component module example - - ```ts - // types.ts - export type MyComponentProps = { - foo: string; - } - - // index.ios.ts - import { MyComponentProps } from "./types"; - - export MyComponentProps; - export default function MyComponent({ foo }: MyComponentProps) { /* ios specific implementation */ } - - // index.ts - import { MyComponentProps } from "./types"; - - export MyComponentProps; - export default function MyComponent({ foo }: MyComponentProps) { /* Default implementation */ } - ``` - - - -- [1.16](#reusable-types) **Reusable Types**: Reusable type definitions, such as models (e.g., Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. - - ```ts - // src/types/Report.ts - - type Report = {...}; - - export default Report; - ``` - - - -- [1.17](#tsx) **tsx**: Use `.tsx` extension for files that contain React syntax. - - > Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. - - - -- [1.18](#no-inline-prop-types) **No inline prop types**: Do not define prop types inline for components that are exported. - - > Why? Prop types might [need to be exported from component files](#export-prop-types). If the component is only used inside a file or module and not exported, then inline prop types can be used. - - ```ts - // BAD - export default function MyComponent({ foo, bar }: { foo: string, bar: number }){ - // component implementation - }; - - // GOOD - type MyComponentProps = { foo: string, bar: number }; - export default MyComponent({ foo, bar }: MyComponentProps){ - // component implementation - } - ``` - - - -- [1.19](#satisfies-operator) **Satisfies Operator**: Use the `satisfies` operator when you need to validate that the structure of an expression matches a specific type, without affecting the resulting type of the expression. - - > Why? TypeScript developers often want to ensure that an expression aligns with a certain type, but they also want to retain the most specific type possible for inference purposes. The `satisfies` operator assists in doing both. - - ```ts - // BAD - const sizingStyles = { - w50: { - width: '50%', - }, - mw100: { - maxWidth: '100%', - }, - } as const; - - // GOOD - const sizingStyles = { - w50: { - width: '50%', - }, - mw100: { - maxWidth: '100%', - }, - } satisfies Record; - ``` - - - -- [1.20](#hooks-instead-of-hocs) **Hooks instead of HOCs**: Replace HOCs usage with Hooks whenever possible. - - > Why? Hooks are easier to use (can be used inside the function component), and don't need nesting or `compose` when exporting the component. It also allows us to remove `compose` completely in some components since it has been bringing up some issues with TypeScript. Read the [`compose` usage](#compose-usage) section for further information about the TypeScript issues with `compose`. - - > Note: Because Onyx doesn't provide a hook yet, in a component that accesses Onyx data with `withOnyx` HOC, please make sure that you don't use other HOCs (if applicable) to avoid HOC nesting. - - ```tsx - // BAD - type ComponentOnyxProps = { - session: OnyxEntry; - }; - - type ComponentProps = WindowDimensionsProps & - WithLocalizeProps & - ComponentOnyxProps & { - someProp: string; - }; - - function Component({windowWidth, windowHeight, translate, session, someProp}: ComponentProps) { - // component's code - } - - export default compose( - withWindowDimensions, - withLocalize, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), - )(Component); - - // GOOD - type ComponentOnyxProps = { - session: OnyxEntry; - }; - - type ComponentProps = ComponentOnyxProps & { - someProp: string; - }; - - function Component({session, someProp}: ComponentProps) { - const {windowWidth, windowHeight} = useWindowDimensions(); - const {translate} = useLocalize(); - // component's code - } - - // There is no hook alternative for withOnyx yet. - export default withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component); - ``` - - - -- [1.21](#compose-usage) **`compose` usage**: Avoid the usage of `compose` function to compose HOCs in TypeScript files. Use nesting instead. - - > Why? `compose` function doesn't work well with TypeScript when dealing with several HOCs being used in a component, many times resulting in wrong types and errors. Instead, nesting can be used to allow a seamless use of multiple HOCs and result in a correct return type of the compoment. Also, you can use [hooks instead of HOCs](#hooks-instead-of-hocs) whenever possible to minimize or even remove the need of HOCs in the component. - - ```ts - // BAD - export default compose( - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), - )(Component); - - // GOOD - export default withCurrentUserPersonalDetails( - withReportOrNotFound()( - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component), - ), - ); - - // GOOD - alternative to HOC nesting - const ComponentWithOnyx = withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component); - const ComponentWithReportOrNotFound = withReportOrNotFound()(ComponentWithOnyx); - export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound); - ``` - - - -- [1.22](#type-imports) **Type imports/exports**: Always use the `type` keyword when importing/exporting types - - > Why? In order to improve code clarity and consistency and reduce bundle size after typescript transpilation, we enforce the all type imports/exports to contain the `type` keyword. This way, TypeScript can automatically remove those imports from the transpiled JavaScript bundle - - Imports: - ```ts - // BAD - import {SomeType} from './a' - import someVariable from './a' - - import {someVariable, SomeOtherType} from './b' - - // GOOD - import type {SomeType} from './a' - import someVariable from './a' - ``` - - Exports: - ```ts - // BAD - export {SomeType} - export someVariable - // or - export {someVariable, SomeOtherType} - - // GOOD - export type {SomeType} - export someVariable - ``` - -## Exception to Rules - -Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily. - -When an exception is granted, link the relevant Slack conversation in your PR. Suppress ESLint or TypeScript warnings/errors with comments if necessary. - -This rule will apply until the migration is done. After the migration, discussion on granting exception can happen inside the PR page and doesn't need take place in the Slack channel. - -## Communication Items - -> Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. - -- I think types definitions in a third party library or JavaScript's built-in module are incomplete or incorrect - -When the library indeed contains incorrect or missing type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/types/modules/*.d.ts`, each library as a separate file. - -```ts -// external-library-name.d.ts - -declare module "external-library-name" { - interface LibraryComponentProps { - // Add or modify typings - additionalProp: string; - } -} -``` - -## Migration Guidelines - -> This section contains instructions that are applicable during the migration. - -- 🚨 Any new files under `src/` directory MUST be created in TypeScript now! New files in other directories (e.g. `tests/`, `desktop/`) can be created in TypeScript, if desired. - -- If you're migrating a module that doesn't have a default implementation (i.e. `index.ts`, e.g. `getPlatform`), convert `index.website.js` to `index.ts`. Without `index.ts`, TypeScript cannot get type information where the module is imported. - -- Deprecate the usage of `underscore`. Use vanilla methods from JS instead. Only use `lodash` when there is no easy vanilla alternative (eg. `lodashMerge`). eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) - -```ts -// BAD -var arr = []; -_.each(arr, () => {}); - -// GOOD -var arr = []; -arr.forEach(() => {}); - -// BAD -lodashGet(object, ['foo'], 'bar'); - -// GOOD -object?.foo ?? 'bar'; -``` - -- Found type bugs. Now what? - - If TypeScript migration uncovers a bug that has been “invisible,” there are two options an author of a migration PR can take: - - - Fix issues if they are minor. Document each fix in the PR comment. - - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue. On the same line as `@ts-expect-error`, put down the GH issue number prefixed with `TODO:`. - - > The `@ts-expect-error` annotation tells the TS compiler to ignore any errors in the line that follows it. However, if there's no error in the line, TypeScript will also raise an error. - - ```ts - // @ts-expect-error TODO: #21647 - const x: number = "123"; // No TS error raised - - // @ts-expect-error - const y: number = 123; // TS error: Unused '@ts-expect-error' directive. - ``` - -- The TS issue I'm working on is blocked by another TS issue because of type errors. What should I do? - - In order to proceed with the migration faster, we are now allowing the use of `@ts-expect-error` annotation to temporally suppress those errors and help you unblock your issues. The only requirements is that you MUST add the annotation with a comment explaining that it must be removed when the blocking issue is migrated, e.g.: - - ```tsx - return ( - - ); - ``` - - **You will also need to reference the blocking issue in your PR.** You can find all the TS issues [here](https://github.com/orgs/Expensify/projects/46). - -## Learning Resources - -### Quickest way to learn TypeScript - -- Get up to speed quickly - - [TypeScript playground](https://www.typescriptlang.org/play?q=231#example) - - Go though all examples on the playground. Click on "Example" tab on the top -- Handy Reference - - [TypeScript CheatSheet](https://www.typescriptlang.org/cheatsheets) - - [Type](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) - - [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) -- TypeScript with React - - [React TypeScript CheatSheet](https://react-typescript-cheatsheet.netlify.app/) - - [List of built-in utility types](https://react-typescript-cheatsheet.netlify.app/docs/basic/troubleshooting/utilities) - - [HOC CheatSheet](https://react-typescript-cheatsheet.netlify.app/docs/hoc/) From 6ba70347fc110df5320759e7927f3135e4715e8c Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 29 Apr 2024 16:50:22 +0200 Subject: [PATCH 057/219] =?UTF-8?q?[TS=20migration]=20Migrate=20MoneyTempo?= =?UTF-8?q?raryForRefactorRequestParticipantsSelector.js=C2=A0to=C2=A0Mone?= =?UTF-8?q?yRequestParticipantsSelector.tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CONST.ts | 3 +- src/components/ReferralProgramCTA.tsx | 4 +- ...s => MoneyRequestParticipantsSelector.tsx} | 115 ++++++++---------- .../step/IOURequestStepParticipants.tsx | 2 +- src/types/onyx/IOU.ts | 1 + 5 files changed, 59 insertions(+), 66 deletions(-) rename src/pages/iou/request/{MoneyTemporaryForRefactorRequestParticipantsSelector.js => MoneyRequestParticipantsSelector.tsx} (77%) diff --git a/src/CONST.ts b/src/CONST.ts index 83690d5e9a85..dde80daf8a32 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4718,7 +4718,8 @@ type Country = keyof typeof CONST.ALL_COUNTRIES; type IOUType = ValueOf; type IOUAction = ValueOf; +type IOURequestType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType}; export default CONST; diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 237fc8f955a3..086b875451c0 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -1,5 +1,5 @@ import React, {useEffect} from 'react'; -import type {ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; @@ -19,7 +19,7 @@ type ReferralProgramCTAProps = { | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.PAY_SOMEONE | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; - style?: ViewStyle; + style?: StyleProp; onDismiss?: () => void; }; diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx similarity index 77% rename from src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js rename to src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 7ae6d25e1b4f..47279c48fea0 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -1,12 +1,8 @@ -import lodashGet from 'lodash/get'; import lodashIsEqual from 'lodash/isEqual'; -import lodashMap from 'lodash/map'; import lodashPick from 'lodash/pick'; import lodashReject from 'lodash/reject'; -import lodashSome from 'lodash/some'; -import lodashValues from 'lodash/values'; -import PropTypes from 'prop-types'; import React, {memo, useCallback, useEffect, useMemo} from 'react'; +import type {GestureResponderEvent} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -23,46 +19,43 @@ import usePermissions from '@hooks/usePermissions'; import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import type {MaybePhraseKey} from '@libs/Localize'; +import type {Options} from '@libs/OptionsListUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as Policy from '@userActions/Policy'; import * as Report from '@userActions/Report'; +import type {IOUAction, IOURequestType, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Participant} from '@src/types/onyx/IOU'; -const propTypes = { +type MoneyRequestParticipantsSelectorProps = { /** Callback to request parent modal to go to next step, which should be split */ - onFinish: PropTypes.func.isRequired, + onFinish: (value?: string) => void; /** Callback to add participants in MoneyRequestModal */ - onParticipantsAdded: PropTypes.func.isRequired, - + onParticipantsAdded: (value: Participant[]) => void; /** Selected participants from MoneyRequestModal with login */ - participants: PropTypes.arrayOf( - PropTypes.shape({ - accountID: PropTypes.number, - login: PropTypes.string, - isPolicyExpenseChat: PropTypes.bool, - isOwnPolicyExpenseChat: PropTypes.bool, - selected: PropTypes.bool, - }), - ), + participants?: Participant[]; /** The type of IOU report, i.e. split, request, send, track */ - iouType: PropTypes.oneOf(lodashValues(CONST.IOU.TYPE)).isRequired, + iouType: IOUType; /** The expense type, ie. manual, scan, distance */ - iouRequestType: PropTypes.oneOf(lodashValues(CONST.IOU.REQUEST_TYPE)).isRequired, + iouRequestType: IOURequestType; /** The action of the IOU, i.e. create, split, move */ - action: PropTypes.oneOf(lodashValues(CONST.IOU.ACTION)), -}; - -const defaultProps = { - participants: [], - action: CONST.IOU.ACTION.CREATE, + action?: IOUAction; }; -function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onFinish, onParticipantsAdded, iouType, iouRequestType, action}) { +function MoneyRequestParticipantsSelector({ + participants = [], + onFinish, + onParticipantsAdded, + iouType, + iouRequestType, + action = CONST.IOU.ACTION.CREATE, +}: MoneyRequestParticipantsSelectorProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); @@ -78,12 +71,12 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF shouldInitialize: didScreenTransitionEnd, }); - const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; + const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; - const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].includes(action); + const isCategorizeOrShareAction = ([CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE] as string[]).includes(action); const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE; @@ -97,7 +90,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF * @returns {Array} */ const [sections, newChatOptions] = useMemo(() => { - const newSections = []; + const newSections: OptionsListUtils.CategorySection[] = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, {}]; } @@ -113,14 +106,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF // sees the option to submit an expense from their admin on their own Workspace Chat. (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.SPLIT) && action !== CONST.IOU.ACTION.SUBMIT, - (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, + (canUseP2PDistanceRequests ?? iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, {}, [], false, {}, [], - (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, + (canUseP2PDistanceRequests ?? iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, undefined, undefined, @@ -134,7 +127,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( debouncedSearchTerm, - participants, + participants.map((participant) => ({...participant, reportID: participant.reportID ?? ''})), chatOptions.recentReports, chatOptions.personalDetails, maxParticipantsReached, @@ -160,11 +153,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF shouldShow: chatOptions.personalDetails.length > 0, }); - if (chatOptions.userToInvite && !OptionsListUtils.isCurrentUser(chatOptions.userToInvite)) { + if ( + chatOptions.userToInvite && + !OptionsListUtils.isCurrentUser({...chatOptions.userToInvite, accountID: chatOptions.userToInvite?.accountID ?? -1, status: chatOptions.userToInvite?.status ?? undefined}) + ) { newSections.push({ title: undefined, - data: lodashMap([chatOptions.userToInvite], (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + data: [chatOptions.userToInvite].map((participant) => { + const isPolicyExpenseChat = participant?.isPolicyExpenseChat ?? false; return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), shouldShow: true, @@ -196,8 +192,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF * @param {Object} option */ const addSingleParticipant = useCallback( - (option) => { - const newParticipants = [ + (option: Participant) => { + const newParticipants: Participant[] = [ { ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), selected: true, @@ -206,10 +202,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ]; if (iouType === CONST.IOU.TYPE.INVOICE) { - const primaryPolicy = Policy.getPrimaryPolicy(activePolicyID); + const primaryPolicy = Policy.getPrimaryPolicy(activePolicyID ?? undefined); newParticipants.push({ - policyID: primaryPolicy.id, + policyID: primaryPolicy?.id, isSender: true, selected: false, iouType, @@ -228,20 +224,20 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF * @param {Object} option */ const addParticipantToSelection = useCallback( - (option) => { - const isOptionSelected = (selectedOption) => { - if (selectedOption.accountID && selectedOption.accountID === option.accountID) { + (option: Participant) => { + const isOptionSelected = (selectedOption: Participant) => { + if (selectedOption.accountID && selectedOption.accountID === option?.accountID) { return true; } - if (selectedOption.reportID && selectedOption.reportID === option.reportID) { + if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { return true; } return false; }; - const isOptionInList = lodashSome(participants, isOptionSelected); - let newSelectedOptions; + const isOptionInList = participants.some(isOptionSelected); + let newSelectedOptions: Participant[]; if (isOptionInList) { newSelectedOptions = lodashReject(participants, isOptionSelected); @@ -269,11 +265,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF const headerMessage = useMemo( () => OptionsListUtils.getHeaderMessage( - lodashGet(newChatOptions, 'personalDetails', []).length + lodashGet(newChatOptions, 'recentReports', []).length !== 0, - Boolean(newChatOptions.userToInvite), + ((newChatOptions as Options)?.personalDetails ?? []).length + ((newChatOptions as Options)?.recentReports ?? []).length !== 0, + Boolean((newChatOptions as Options)?.userToInvite), debouncedSearchTerm.trim(), maxParticipantsReached, - lodashSome(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), + participants.some((participant) => participant?.searchText?.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), ), [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], ); @@ -281,17 +277,17 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF // Right now you can't split an expense with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants - const hasPolicyExpenseChatParticipant = lodashSome(participants, (participant) => participant.isPolicyExpenseChat); + const hasPolicyExpenseChatParticipant = participants.some((participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet const isAllowedToSplit = - (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && - ![CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE].includes(iouType) && - ![CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE].includes(action); + (canUseP2PDistanceRequests ?? iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && + !([CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE] as string[]).includes(iouType) && + !([CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE] as string[]).includes(action); const handleConfirmSelection = useCallback( - (keyEvent, option) => { + (keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Participant) => { const shouldAddSingleParticipant = option && !participants.length; if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { return; @@ -362,15 +358,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ); } -MoneyTemporaryForRefactorRequestParticipantsSelector.propTypes = propTypes; -MoneyTemporaryForRefactorRequestParticipantsSelector.defaultProps = defaultProps; -MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTemporaryForRefactorRequestParticipantsSelector'; +MoneyRequestParticipantsSelector.displayName = 'MoneyTemporaryForRefactorRequestParticipantsSelector'; export default memo( - MoneyTemporaryForRefactorRequestParticipantsSelector, + MoneyRequestParticipantsSelector, (prevProps, nextProps) => - lodashIsEqual(prevProps.participants, nextProps.participants) && - prevProps.iouRequestType === nextProps.iouRequestType && - prevProps.iouType === nextProps.iouType && - lodashIsEqual(prevProps.betas, nextProps.betas), + lodashIsEqual(prevProps.participants, nextProps.participants) && prevProps.iouRequestType === nextProps.iouRequestType && prevProps.iouType === nextProps.iouType, ); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index be95cb03e95b..3f917a14ae11 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -5,7 +5,7 @@ import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as TransactionUtils from '@libs/TransactionUtils'; -import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector'; +import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index 726b94c5f6d3..82bf4d6efc04 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -23,6 +23,7 @@ type Participant = { isSelected?: boolean; isSelfDM?: boolean; isSender?: boolean; + iouType?: string; }; type Split = { From d58f5fb91908c4ce3272c8adc64374c1145b004e Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 29 Apr 2024 22:11:29 +0530 Subject: [PATCH 058/219] remove redundant eslint comment. Signed-off-by: Krishna Gupta --- src/components/SelectionList/RadioListItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx index 0c57023c1d24..b595008e4e3b 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.tsx @@ -52,7 +52,6 @@ function RadioListItem({ styles.sidebarLinkTextBold, isMultilineSupported ? styles.preWrap : styles.pre, item.alternateText ? styles.mb1 : null, - /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ isDisabled && styles.colorMuted, isMultilineSupported ? {paddingLeft} : null, ]} From bde72c23e1a8beb8060095cb6194be15381a9785 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 30 Apr 2024 12:47:00 +0200 Subject: [PATCH 059/219] add table of contents to STYLE.md --- contributingGuides/STYLE.md | 138 +++++++++++++++++++++++++----------- 1 file changed, 97 insertions(+), 41 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 5f4ef84072ae..56a66e9433e7 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -1,5 +1,63 @@ # Coding Standards +## Table of Contents + +- [Introduction](#introduction) +- [TypeScript guidelines](#typescript-guidelines) + - [General rules](#general-rules) + - [`d.ts` extension](#dts-extension) + - [Type Alias vs Interface](#type-alias-vs-interface) + - [Enum vs. Union Type](#enum-vs-union-type) + - [`unknown` vs. `any`](#unknown-vs-any) + - [`T[]` vs. `Array`](#t-vs-arrayt) + - [`@ts-ignore`](#ts-ignore) + - [Type Inference](#type-inference) + - [Utility Types](#utility-types) + - [`object` type](#object-type) + - [Prop Types](#prop-types) + - [File organization](#file-organization) + - [Reusable Types](#reusable-types) + - [`tsx` extension](#tsx-extension) + - [No inline prop types](#no-inline-prop-types) + - [Satisfies Operator](#satisfies-operator) + - [Type imports/exports](#type-importsexports) + - [Exception to Rules](#exception-to-rules) + - [Communication Items](#communication-items) + - [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) +- [Naming Conventions](#naming-conventions) + - [Type names](#type-names) + - [Prop callbacks](#prop-callbacks) + - [Event Handlers](#event-handlers) + - [Boolean variables and props](#boolean-variables-and-props) + - [Functions](#functions) + - [`var`, `const` and `let`](#var-const-and-let) +- [Object / Array Methods](#object--array-methods) +- [Accessing Object Properties and Default Values](#accessing-object-properties-and-default-values) +- [JSDocs](#jsdocs) +- [Component props](#component-props) +- [Destructuring](#destructuring) +- [Named vs Default Exports in ES6 - When to use what?](#named-vs-default-exports-in-es6---when-to-use-what) +- [Classes and constructors](#classes-and-constructors) + - [Class syntax](#class-syntax) + - [Constructor](#constructor) +- [ESNext: Are we allowed to use [insert new language feature]? Why or why not?](#esnext-are-we-allowed-to-use-insert-new-language-feature-why-or-why-not) +- [React Coding Standards](#react-coding-standards) + - [Code Documentation](#code-documentation) + - [Inline Ternaries](#inline-ternaries) + - [Function component style](#function-component-style) + - [Forwarding refs](#forwarding-refs) + - [Hooks and HOCs](#hooks-and-hocs) + - [Stateless components vs Pure Components vs Class based components vs Render Props](#stateless-components-vs-pure-components-vs-class-based-components-vs-render-props---when-to-use-what) + - [Composition](#composition) + - [Use Refs Appropriately](#use-refs-appropriately) + - [Are we allowed to use [insert brand new React feature]?](#are-we-allowed-to-use-insert-brand-new-react-feature-why-or-why-not) +- [React Hooks: Frequently Asked Questions](#react-hooks-frequently-asked-questions) +- [Onyx Best Practices](#onyx-best-practices) + - [Collection Keys](#collection-keys) +- [Learning Resources](#learning-resources) + +## Introduction + For almost all of our code style rules, refer to the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). @@ -13,7 +71,7 @@ There are a few things that we have customized for our tastes which will take pr ## TypeScript guidelines -### General Rules +### General rules Strive to type as strictly as possible. @@ -113,7 +171,7 @@ const e: string[] = ["a", "b"]; const f: readonly string[] = ["a", "b"]; ``` -### @ts-ignore +### `@ts-ignore` Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. @@ -290,7 +348,7 @@ type Report = {...}; export default Report; ``` -### tsx +### `tsx` extension Use `.tsx` extension for files that contain React syntax. @@ -408,7 +466,7 @@ declare module "external-library-name" { ## Naming Conventions -### Types +### Type names - Use PascalCase for type names. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) @@ -548,7 +606,7 @@ const valid = props.something && props.somethingElse; const isValid = props.something && props.somethingElse; ``` -## Functions +### Functions Any function declared in a library module should use the `function myFunction` keyword rather than `const myFunction`. @@ -650,31 +708,31 @@ if (someCondition) { ## Object / Array Methods -We have standardized on using [underscore.js](https://underscorejs.org/) methods for objects and collections instead of the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods). This is mostly to maintain consistency, but there are some type safety features and conveniences that underscore methods provide us e.g. the ability to iterate over an object and the lack of a `TypeError` thrown if a variable is `undefined`. +We have standardized on using the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods) instead of [underscore.js](https://underscorejs.org/) methods for objects and collections. As the vast majority of code is written in TypeScript, we can safaly use the native methods. ```ts // Bad -myArray.forEach(item => doSomething(item)); -// Good _.each(myArray, item => doSomething(item)); +// Good +myArray.forEach(item => doSomething(item)); // Bad -const myArray = Object.keys(someObject).map((key) => doSomething(someObject[key])); -// Good const myArray = _.map(someObject, (value, key) => doSomething(value)); +// Good +const myArray = Object.keys(someObject).map((key) => doSomething(someObject[key])); // Bad -myCollection.includes('item'); -// Good _.contains(myCollection, 'item'); +// Good +myCollection.includes('item'); // Bad -const modifiedArray = someArray.filter(filterFunc).map(mapFunc); -// Good const modifiedArray = _.chain(someArray) .filter(filterFunc) .map(mapFunc) .value(); +// Good +const modifiedArray = someArray.filter(filterFunc).map(mapFunc); ``` ## Accessing Object Properties and Default Values @@ -787,10 +845,10 @@ export { ## Classes and constructors -#### Class syntax +### Class syntax Using the `class` syntax is preferred wherever appropriate. Airbnb has clear [guidelines](https://github.com/airbnb/javascript#classes--constructors) in their JS style guide which promotes using the _class_ syntax. Don't manipulate the `prototype` directly. The `class` syntax is generally considered more concise and easier to understand. -#### Constructor +### Constructor Classes have a default constructor if one is not specified. No need to write a constructor function that is empty or just delegates to a parent class. ```js @@ -831,11 +889,9 @@ Here are a couple of things we would ask that you *avoid* to help maintain consi - **Optional Chaining** - Yes, don't use `lodashGet()` - **Null Coalescing Operator** - Yes, don't use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable -# React Coding Standards - -# React specific styles +## React Coding Standards -## Code Documentation +### Code Documentation * Add descriptions to all component props using a block comment above the definition. No need to document the types, but add some context for each property so that other developers understand the intended use. @@ -872,7 +928,7 @@ type ComponentProps = { } ``` -## Inline Ternaries +### Inline Ternaries * Use inline ternary statements when rendering optional pieces of templates. Notice the white space and formatting of the ternary. ```tsx @@ -917,7 +973,7 @@ type ComponentProps = { } ``` -### Important Note: +#### Important Note: In React Native, one **must not** attempt to falsey-check a string for an inline ternary. Even if it's in curly braces, React Native will try to render it as a `` node and most likely throw an error about trying to render text outside of a `` component. Use `!!` instead. @@ -947,7 +1003,7 @@ In React Native, one **must not** attempt to falsey-check a string for an inline } ``` -## Function component style +### Function component style When writing a function component, you must ALWAYS add a `displayName` property and give it the same value as the name of the component (this is so it appears properly in the React dev tools) @@ -959,7 +1015,7 @@ Avatar.displayName = 'Avatar'; export default Avatar; ``` -## Forwarding refs +### Forwarding refs When forwarding a ref define named component and pass it directly to the `forwardRef`. By doing this, we remove potential extra layer in React tree in the form of anonymous component. @@ -1003,7 +1059,7 @@ function FancyInput(props: FancyInputProps, ref: ForwardedRef) export default React.forwardRef(FancyInput) ``` -## Hooks and HOCs +### Hooks and HOCs Use hooks whenever possible, avoid using HOCs. @@ -1060,13 +1116,13 @@ export default withOnyx({ })(Component); ``` -## Stateless components vs Pure Components vs Class based components vs Render Props - When to use what? +### Stateless components vs Pure Components vs Class based components vs Render Props - When to use what? Class components are DEPRECATED. Use function components and React hooks. [https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function](https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function) -## Composition +### Composition Avoid the usage of `compose` function to compose HOCs in TypeScript files. Use nesting instead. @@ -1111,7 +1167,7 @@ From React's documentation - **Note:** If you find that none of these approaches work for you, please ask an Expensify engineer for guidance via Slack or GitHub. -## Use Refs Appropriately +### Use Refs Appropriately React's documentation explains refs in [detail](https://reactjs.org/docs/refs-and-the-dom.html). It's important to understand when to use them and how to use them to avoid bugs and hard to maintain code. @@ -1119,41 +1175,41 @@ A common mistake with refs is using them to pass data back to a parent component There are several ways to use and declare refs and we prefer the [callback method](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs). -## Are we allowed to use [insert brand new React feature]? Why or why not? +### Are we allowed to use [insert brand new React feature]? Why or why not? We love React and learning about all the new features that are regularly being added to the API. However, we try to keep our organization's usage of React limited to the most stable set of features that React offers. We do this mainly for **consistency** and so our engineers don't have to spend extra time trying to figure out how everything is working. That said, if you aren't sure if we have adopted something, please ask us first. -# React Hooks: Frequently Asked Questions +## React Hooks: Frequently Asked Questions -## Are Hooks a Replacement for HOCs or Render Props? +### Are Hooks a Replacement for HOCs or Render Props? In most cases, a custom hook is a better pattern to use than an HOC or Render Prop. They are easier to create, understand, use and document. However, there might still be a case for a HOC e.g. if you have a component that abstracts some conditional rendering logic. -## Should I wrap all my inline functions with `useCallback()` or move them out of the component if they have no dependencies? +### Should I wrap all my inline functions with `useCallback()` or move them out of the component if they have no dependencies? The answer depends on whether you need a stable reference for the function. If there are no dependencies, you could move the function out of the component. If there are dependencies, you could use `useCallback()` to ensure the reference updates only when the dependencies change. However, it's important to note that using `useCallback()` may have a performance penalty, although the trade-off is still debated. You might choose to do nothing at all if there is no obvious performance downside to declaring a function inline. It's recommended to follow the guidance in the [React documentation](https://react.dev/reference/react/useCallback#should-you-add-usecallback-everywhere) and add the optimization only if necessary. If it's not obvious why such an optimization (i.e. `useCallback()` or `useMemo()`) would be used, leave a code comment explaining the reasoning to aid reviewers and future contributors. -## Why does `useState()` sometimes get initialized with a function? +### Why does `useState()` sometimes get initialized with a function? React saves the initial state once and ignores it on the next renders. However, if you pass the result of a function to `useState()` or call a function directly e.g. `useState(doExpensiveThings())` it will *still run on every render*. This can hurt performance depending on what work the function is doing. As an optimization, we can pass an initializer function instead of a value e.g. `useState(doExpensiveThings)` or `useState(() => doExpensiveThings())`. -## Is there an equivalent to `componentDidUpdate()` when using hooks? +### Is there an equivalent to `componentDidUpdate()` when using hooks? The short answer is no. A longer answer is that sometimes we need to check not only that a dependency has changed, but how it has changed in order to run a side effect. For example, a prop had a value of an empty string on a previous render, but now is non-empty. The generally accepted practice is to store the "previous" value in a `ref` so the comparison can be made in a `useEffect()` call. -## Are `useCallback()` and `useMemo()` basically the same thing? +### Are `useCallback()` and `useMemo()` basically the same thing? No! It is easy to confuse `useCallback()` with a memoization helper like `_.memoize()` or `useMemo()` but they are really not the same at all. [`useCallback()` will return a cached function _definition_](https://react.dev/reference/react/useCallback) and will not save us any computational cost of running that function. So, if you are wrapping something in a `useCallback()` and then calling it in the render, then it is better to use `useMemo()` to cache the actual **result** of calling that function and use it directly in the render. -## What is the `exhaustive-deps` lint rule? Can I ignore it? +### What is the `exhaustive-deps` lint rule? Can I ignore it? A `useEffect()` that does not include referenced props or state in its dependency array is [usually a mistake](https://legacy.reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies) as often we want effects to re-run when those dependencies change. However, there are some cases where we might actually only want to re-run the effect when only some of those dependencies change. We determined the best practice here should be to allow disabling the “next line” with a comment `//eslint-disable-next-line react-hooks/exhaustive-deps` and an additional comment explanation so the next developer can understand why the rule was not used. -## Should I declare my components with arrow functions (`const`) or the `function` keyword? +### Should I declare my components with arrow functions (`const`) or the `function` keyword? There are pros and cons of each, but ultimately we have standardized on using the `function` keyword to align things more with modern React conventions. There are also some minor cognitive overhead benefits in that you don't need to think about adding and removing brackets when encountering an implicit return. The `function` syntax also has the benefit of being able to be hoisted where arrow functions do not. -## How do I auto-focus a TextInput using `useFocusEffect()`? +### How do I auto-focus a TextInput using `useFocusEffect()`? ```tsx const focusTimeoutRef = useRef(null); @@ -1175,11 +1231,11 @@ This works better than using `onTransitionEnd` because - Note - This is a solution from [this PR](https://github.com/Expensify/App/pull/26415). You can find detailed discussion in comments. -# Onyx Best Practices +## Onyx Best Practices [Onyx Documentation](https://github.com/expensify/react-native-onyx) -## Collection Keys +### Collection Keys Our potentially larger collections of data (reports, policies, etc) are typically stored under collection keys. Collection keys let us group together individual keys vs. storing arrays with multiple objects. In general, **do not add a new collection key if it can be avoided**. There is most likely a more logical place to put the state. And failing to associate a state property with its logical owner is something we consider to be an anti-pattern (unnecessary data structure adds complexity for no value). @@ -1189,7 +1245,7 @@ For example, if you are storing a boolean value that could be associated with a If you're not sure whether something should have a collection key reach out in [`#expensify-open-source`](https://expensify.slack.com/archives/C01GTK53T8Q) for additional feedback. -# Learning Resources +## Learning Resources ### Quickest way to learn TypeScript From a7f8a29f3c1398edad4231cdf89ae24f9e8de8db Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 1 May 2024 10:57:54 +0530 Subject: [PATCH 060/219] fix: Jest Unit Tests. Signed-off-by: Krishna Gupta --- tests/unit/OptionsListUtilsTest.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 76b4324f697b..0df4e2fe124b 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1067,8 +1067,8 @@ describe('OptionsListUtils', () => { keyForList: 'Medical', searchText: 'Medical', tooltipText: 'Medical', - isDisabled: false, - isSelected: false, + isDisabled: true, + isSelected: true, }, ], }, @@ -1236,8 +1236,8 @@ describe('OptionsListUtils', () => { keyForList: 'Medical', searchText: 'Medical', tooltipText: 'Medical', - isDisabled: false, - isSelected: false, + isDisabled: true, + isSelected: true, }, ], }, @@ -2587,6 +2587,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax exempt 1 (0%) • Default', tooltipText: 'Tax exempt 1 (0%) • Default', isDisabled: undefined, + isSelected: undefined, // creates a data option. data: { name: 'Tax exempt 1', @@ -2601,6 +2602,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax option 3 (5%)', tooltipText: 'Tax option 3 (5%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax option 3', code: 'CODE3', @@ -2614,6 +2616,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax rate 2', code: 'CODE2', @@ -2637,6 +2640,7 @@ describe('OptionsListUtils', () => { searchText: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', isDisabled: undefined, + isSelected: undefined, data: { name: 'Tax rate 2', code: 'CODE2', From 7938e75f4b7999aa842ed6643d718d4a66e54332 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Wed, 1 May 2024 20:18:36 +0530 Subject: [PATCH 061/219] feat: basic screens and navigation setup --- src/CONST.ts | 1 + src/ROUTES.ts | 12 ++++++++++++ src/SCREENS.ts | 3 +++ src/languages/en.ts | 6 ++++++ src/languages/es.ts | 6 ++++++ .../AppNavigator/ModalStackNavigators/index.tsx | 3 +++ .../linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts | 3 +++ src/libs/Navigation/linkingConfig/config.ts | 3 +++ .../workspace/accounting/xero/XeroImportPage.tsx | 2 +- .../accounting/xero/XeroMapCostCentersToPage.tsx | 10 ++++++++++ .../accounting/xero/XeroMapRegionsToPage.tsx | 10 ++++++++++ .../accounting/xero/XeroTrackCategoriesPage.tsx | 10 ++++++++++ 12 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx create mode 100644 src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx create mode 100644 src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 9d3042b64f80..411be788b645 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1268,6 +1268,7 @@ const CONST = { XERO_CONFIG: { IMPORT_TAX_RATES: 'importTaxRates', + TRACK_CATEGORIES: 'trackCategories', }, QUICKBOOKS_EXPORT_ENTITY: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 801914fc1515..c25dccb4a171 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -774,6 +774,18 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/organization/:currentOrganizationID', getRoute: (policyID: string, currentOrganizationID: string) => `settings/workspaces/${policyID}/accounting/xero/organization/${currentOrganizationID}` as const, }, + POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES: { + route: 'settings/workspaces/:policyID/accounting/xero/import/trackCategories', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/trackCategories` as const, + }, + POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS: { + route: 'settings/workspaces/:policyID/accounting/xero/import/trackCategories/mapCostCenters', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/trackCategories/mapCostCenters` as const, + }, + POLICY_ACCOUNTING_XERO_MAP_REGIONS: { + route: 'settings/workspaces/:policyID/accounting/xero/import/trackCategories/mapRegions', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/trackCategories/mapRegions` as const, + }, POLICY_ACCOUNTING_XERO_TAXES: { route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/taxes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 56363b09c980..bd4e9783650b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -237,6 +237,9 @@ const SCREENS = { XERO_IMPORT: 'Policy_Accounting_Xero_Import', XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', + XERO_TRACK_CATEGORIES: 'Policy_Accounting_Xero_Track_Categories', + XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', + XERO_MAP_REGIONS: 'Policy_Accounting_Xero_Map_Regions' }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', diff --git a/src/languages/en.ts b/src/languages/en.ts index 4365ce6a6a88..f565dc073db9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1993,6 +1993,12 @@ export default { organizationDescription: 'Select the organization in Xero you are importing data from.', importDescription: 'Choose which coding configurations are imported from Xero to Expensify.', trackingCategories: 'Tracking categories', + trackingCategoriesDescription: 'Choose whether to import tracking categories and see where they are displayed.', + mapXeroCostCentersTo: 'Map Xero cost centers to', + mapXeroRegionsTo: 'Map Xero regions to', + mapXeroCostCentersToDescription: 'Choose where to map cost centers to when exporting to Xero.', + mapXeroCostRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', + xeroContactDefault: 'Xero contact default', customers: 'Re-bill customers', taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', notImported: 'Not imported', diff --git a/src/languages/es.ts b/src/languages/es.ts index 3301734636ef..c92d431b43e1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2020,6 +2020,12 @@ export default { organizationDescription: 'Seleccione la organización en Xero desde la que está importando los datos.', importDescription: 'Elija qué configuraciones de codificación se importan de Xero a Expensify.', trackingCategories: 'Categorías de seguimiento', + trackingCategoriesDescription: 'Choose whether to import tracking categories and see where they are displayed.', + mapXeroCostCentersTo: 'Map Xero cost centers to', + mapXeroRegionsTo: 'Map Xero regions to', + mapXeroCostCentersToDescription: 'Choose where to map cost centers to when exporting to Xero.', + mapXeroCostRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', + xeroContactDefault: 'Xero contact default', customers: 'Volver a facturar a los clientes', taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', notImported: 'No importado', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 79fe3628a782..7e045f879582 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -296,6 +296,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/XeroImportPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: () => require('../../../../pages/workspace/accounting/xero/XeroOrganizationConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: () => require('../../../../pages/workspace/accounting/xero/XeroTaxesConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: () => require('../../../../pages/workspace/accounting/xero/XeroTrackCategoriesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapCostCentersToPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapRegionsToPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 7712d1fc56a8..0c5953e0ee99 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -42,6 +42,9 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT, SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION, SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES, + SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES, + SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS, + SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS, ], [SCREENS.WORKSPACE.TAXES]: [ SCREENS.WORKSPACE.TAXES_SETTINGS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index fb27b9b63447..3d7df5c541cb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -326,6 +326,9 @@ const config: LinkingOptions['config'] = { }, [SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: {path: ROUTES.POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS]: {path: ROUTES.POLICY_ACCOUNTING_XERO_MAP_REGIONS.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TAXES.route}, [SCREENS.WORKSPACE.DESCRIPTION]: { path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route, diff --git a/src/pages/workspace/accounting/xero/XeroImportPage.tsx b/src/pages/workspace/accounting/xero/XeroImportPage.tsx index af36bfcc42cd..180b977e56a5 100644 --- a/src/pages/workspace/accounting/xero/XeroImportPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroImportPage.tsx @@ -36,7 +36,7 @@ function XeroImportPage({policy}: WithPolicyProps) { }, { description: translate('workspace.xero.trackingCategories'), - action: () => {}, + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.getRoute(policyID)), hasError: !!policy?.errors?.importTrackingCategories, title: importTrackingCategories ? translate('workspace.accounting.importedAsTags') : translate('workspace.xero.notImported'), pendingAction: pendingFields?.importTrackingCategories, diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx new file mode 100644 index 000000000000..0bf7ffd044ea --- /dev/null +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +function XeroMapCostCentersToPage() { + return <> + Xero Cost Centers + +} + +XeroMapCostCentersToPage.displayName = 'XeroMapCostCentersToPage'; +export default XeroMapCostCentersToPage; \ No newline at end of file diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx new file mode 100644 index 000000000000..682a169b8039 --- /dev/null +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +function XeroMapRegionsToPage() { + return <> + XeroMapRegionsToPage + +} + +XeroMapRegionsToPage.displayName = 'XeroMapRegionsToPage'; +export default XeroMapRegionsToPage; \ No newline at end of file diff --git a/src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx new file mode 100644 index 000000000000..3da0c6bef2ad --- /dev/null +++ b/src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +function XeroTrackCategoriesPage() { + return <> + Xero Track Categories + +} + +XeroTrackCategoriesPage.displayName = 'XeroTrackCategoriesPage'; +export default XeroTrackCategoriesPage; \ No newline at end of file From 5a12e040676bc5b24f31ed654605a450c17b7393 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Thu, 2 May 2024 00:36:06 +0530 Subject: [PATCH 062/219] feat: added more config pages --- src/CONST.ts | 2 +- src/SCREENS.ts | 2 +- .../ModalStackNavigators/index.tsx | 6 +- src/libs/Navigation/types.ts | 3 + .../XeroMapCostCentersToConfigurationPage.tsx | 59 ++++++++++++ .../xero/XeroMapCostCentersToPage.tsx | 10 -- .../XeroMapRegionsToConfigurationPage.tsx | 8 ++ .../accounting/xero/XeroMapRegionsToPage.tsx | 10 -- .../xero/XeroTrackCategoriesPage.tsx | 10 -- .../XeroTrackingCategoryConfigurationPage.tsx | 93 +++++++++++++++++++ 10 files changed, 168 insertions(+), 35 deletions(-) create mode 100644 src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx delete mode 100644 src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx create mode 100644 src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx delete mode 100644 src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx delete mode 100644 src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx create mode 100644 src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 411be788b645..0f28a92db8f6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1268,7 +1268,7 @@ const CONST = { XERO_CONFIG: { IMPORT_TAX_RATES: 'importTaxRates', - TRACK_CATEGORIES: 'trackCategories', + IMPORT_TRACK_CATEGORIES: 'importTrackingCategories', }, QUICKBOOKS_EXPORT_ENTITY: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index bd4e9783650b..2fe314968db2 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -239,7 +239,7 @@ const SCREENS = { XERO_TAXES: 'Policy_Accounting_Xero_Taxes', XERO_TRACK_CATEGORIES: 'Policy_Accounting_Xero_Track_Categories', XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', - XERO_MAP_REGIONS: 'Policy_Accounting_Xero_Map_Regions' + XERO_MAP_REGIONS: 'Policy_Accounting_Xero_Map_Regions', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7e045f879582..59b0d2232c8b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -296,9 +296,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/XeroImportPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: () => require('../../../../pages/workspace/accounting/xero/XeroOrganizationConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: () => require('../../../../pages/workspace/accounting/xero/XeroTaxesConfigurationPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: () => require('../../../../pages/workspace/accounting/xero/XeroTrackCategoriesPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapCostCentersToPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapRegionsToPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: () => require('../../../../pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 7722de72645f..b5bebc400196 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -318,6 +318,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: { + policyID: string; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx new file mode 100644 index 000000000000..cb0ca49ff6fc --- /dev/null +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; + +function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? ''; + + const optionsList = [ + { + value: 'DEFAULT', + text: translate(`workspace.xero.xeroContactDefault`), + keyForList: 'DEFAULT', + }, + { + value: 'TAGS', + text: 'Tags', + keyForList: 'TAGS', + isSelected: true, + }, + ]; + + return ( + + + + + {}} + /> + + + + ); +} + +XeroMapCostCentersToConfigurationPage.displayName = 'XeroMapCostCentersToConfigurationPage'; +export default withPolicyConnections(XeroMapCostCentersToConfigurationPage); diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx deleted file mode 100644 index 0bf7ffd044ea..000000000000 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -function XeroMapCostCentersToPage() { - return <> - Xero Cost Centers - -} - -XeroMapCostCentersToPage.displayName = 'XeroMapCostCentersToPage'; -export default XeroMapCostCentersToPage; \ No newline at end of file diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx new file mode 100644 index 000000000000..a9fd95a41060 --- /dev/null +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +function XeroMapRegionsToConfigurationPage() { + return <>XeroMapRegionsToPage; +} + +XeroMapRegionsToConfigurationPage.displayName = 'XeroMapRegionsToConfigurationPage'; +export default XeroMapRegionsToConfigurationPage; diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx deleted file mode 100644 index 682a169b8039..000000000000 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -function XeroMapRegionsToPage() { - return <> - XeroMapRegionsToPage - -} - -XeroMapRegionsToPage.displayName = 'XeroMapRegionsToPage'; -export default XeroMapRegionsToPage; \ No newline at end of file diff --git a/src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx deleted file mode 100644 index 3da0c6bef2ad..000000000000 --- a/src/pages/workspace/accounting/xero/XeroTrackCategoriesPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -function XeroTrackCategoriesPage() { - return <> - Xero Track Categories - -} - -XeroTrackCategoriesPage.displayName = 'XeroTrackCategoriesPage'; -export default XeroTrackCategoriesPage; \ No newline at end of file diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx new file mode 100644 index 000000000000..3ee0870c2c60 --- /dev/null +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -0,0 +1,93 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Switch from '@components/Switch'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections'; +import Navigation from '@libs/Navigation/Navigation'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? ''; + const {importTrackingCategories, pendingFields} = policy?.connections?.xero?.config ?? {}; + const menuItems = useMemo( + () => [ + { + title: translate('workspace.xero.mapXeroCostCentersTo'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS.getRoute(policyID)), + }, + { + title: translate('workspace.xero.mapXeroRegionsTo'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_REGIONS.getRoute(policyID)), + }, + ], + [translate, policyID], + ); + + return ( + + + + + {translate('workspace.xero.trackingCategoriesDescription')} + + + {translate('workspace.accounting.import')} + + + + + Connections.updatePolicyConnectionConfig( + policyID, + CONST.POLICY.CONNECTIONS.NAME.XERO, + CONST.XERO_CONFIG.IMPORT_TRACK_CATEGORIES, + !importTrackingCategories, + ) + } + /> + + + + + {importTrackingCategories && ( + + {menuItems.map((menuItem) => ( + + ))} + + )} + + + ); +} + +XeroTrackingCategoryConfigurationPage.displayName = 'XeroTrackCategoriesPage'; +export default withPolicyConnections(XeroTrackingCategoryConfigurationPage); From f0f0d415f0351efcb8d4132e05849007d9edd6e4 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Wed, 1 May 2024 12:13:44 -0700 Subject: [PATCH 063/219] Automatic scrolling to report's new unread message (marker) --- .../useReportScrollManager/index.native.ts | 8 +++++-- src/hooks/useReportScrollManager/index.ts | 20 ++++++++--------- src/hooks/useReportScrollManager/types.ts | 2 +- src/pages/home/report/ReportActionsList.tsx | 22 +++++++++++++++++++ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/hooks/useReportScrollManager/index.native.ts b/src/hooks/useReportScrollManager/index.native.ts index 6666a4ebd0f2..20416dd96bf7 100644 --- a/src/hooks/useReportScrollManager/index.native.ts +++ b/src/hooks/useReportScrollManager/index.native.ts @@ -7,13 +7,17 @@ function useReportScrollManager(): ReportScrollManagerData { /** * Scroll to the provided index. + * @param viewPosition (optional) - `0`: top, `0.5`: center, `1`: bottom */ - const scrollToIndex = (index: number) => { + // We're defaulting isEditing to false in order to match the + // number of arguments that index.ts version has. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const scrollToIndex = (index: number, isEditing = false, viewPosition?: number) => { if (!flatListRef?.current) { return; } - flatListRef.current.scrollToIndex({index}); + flatListRef.current.scrollToIndex({index, viewPosition}); }; /** diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts index 0d52dfd63159..11e92991c28e 100644 --- a/src/hooks/useReportScrollManager/index.ts +++ b/src/hooks/useReportScrollManager/index.ts @@ -6,30 +6,28 @@ function useReportScrollManager(): ReportScrollManagerData { const {flatListRef} = useContext(ActionListContext); /** - * Scroll to the provided index. On non-native implementations we do not want to scroll when we are scrolling because + * Scroll to the provided index. + * On non-native implementations we do not want to scroll when we are scrolling because * we are editing a comment. + * @param viewPosition (optional) - `0`: top, `0.5`: center, `1`: bottom */ - const scrollToIndex = (index: number, isEditing?: boolean) => { + const scrollToIndex = (index: number, isEditing?: boolean, viewPosition?: number) => { if (!flatListRef?.current || isEditing) { return; } - flatListRef.current.scrollToIndex({index, animated: true}); + flatListRef.current.scrollToIndex({index, animated: true, viewPosition}); }; /** * Scroll to the bottom of the flatlist. */ const scrollToBottom = useCallback(() => { - // We're deferring execution here because on iOS: mWeb (WebKit based browsers) - // scrollToOffset method doesn't work unless called on the next tick - requestAnimationFrame(() => { - if (!flatListRef?.current) { - return; - } + if (!flatListRef?.current) { + return; + } - flatListRef.current.scrollToOffset({animated: false, offset: 0}); - }); + flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, [flatListRef]); return {ref: flatListRef, scrollToIndex, scrollToBottom}; diff --git a/src/hooks/useReportScrollManager/types.ts b/src/hooks/useReportScrollManager/types.ts index 5182f7269a9c..881114498221 100644 --- a/src/hooks/useReportScrollManager/types.ts +++ b/src/hooks/useReportScrollManager/types.ts @@ -2,7 +2,7 @@ import type {FlatListRefType} from '@pages/home/ReportScreenContext'; type ReportScrollManagerData = { ref: FlatListRefType; - scrollToIndex: (index: number, isEditing?: boolean) => void; + scrollToIndex: (index: number, isEditing?: boolean, viewPosition?: number) => void; scrollToBottom: () => void; }; diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 3c6038697c67..b5a8841ce34f 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -477,6 +477,28 @@ function ReportActionsList({ calculateUnreadMarker(); }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); + useEffect(() => { + const scrollToFirstUnreadMessage = () => { + if (!currentUnreadMarker) { + return; + } + + const unreadMessageIndex = sortedVisibleReportActions.findIndex((action) => action.reportActionID === currentUnreadMarker); + + if (unreadMessageIndex !== -1) { + // We're passing viewPosition: 1 to scroll to the top of the + // unread message (marker) since we're using an inverted FlatList. + reportScrollManager?.scrollToIndex(unreadMessageIndex, false, 1); + } + }; + + // Call the scroll function after a small delay to ensure all items + // have been measured and the list is ready to be scrolled. + InteractionManager.runAfterInteractions(scrollToFirstUnreadMessage); + // We only want to run this effect once, when we're navigating to a report with unread messages. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sortedVisibleReportActions]); + useEffect(() => { if (!userActiveSince.current || report.reportID !== prevReportID) { return; From 605912d2c11b7b81e1e1985ee2fdebfc3dd64def Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Thu, 2 May 2024 00:44:39 +0530 Subject: [PATCH 064/219] refactor: update the regions to page --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- .../XeroMapCostCentersToConfigurationPage.tsx | 9 +-- .../XeroMapRegionsToConfigurationPage.tsx | 58 ++++++++++++++++++- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f565dc073db9..6350559dc2f7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1997,7 +1997,7 @@ export default { mapXeroCostCentersTo: 'Map Xero cost centers to', mapXeroRegionsTo: 'Map Xero regions to', mapXeroCostCentersToDescription: 'Choose where to map cost centers to when exporting to Xero.', - mapXeroCostRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', + mapXeroRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', xeroContactDefault: 'Xero contact default', customers: 'Re-bill customers', taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', diff --git a/src/languages/es.ts b/src/languages/es.ts index c92d431b43e1..570038662769 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2024,7 +2024,7 @@ export default { mapXeroCostCentersTo: 'Map Xero cost centers to', mapXeroRegionsTo: 'Map Xero regions to', mapXeroCostCentersToDescription: 'Choose where to map cost centers to when exporting to Xero.', - mapXeroCostRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', + mapXeroRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', xeroContactDefault: 'Xero contact default', customers: 'Volver a facturar a los clientes', taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index cb0ca49ff6fc..56557fdd4d13 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {View} from 'react-native'; +import Text from '@components/Text'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -42,14 +42,15 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { shouldEnableMaxHeight testID={XeroMapCostCentersToConfigurationPage.displayName} > - + - {translate('workspace.xero.mapXeroCostCentersToDescription')} +
+ {}} /> -
); diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index a9fd95a41060..20032d8538bd 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -1,8 +1,60 @@ import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; -function XeroMapRegionsToConfigurationPage() { - return <>XeroMapRegionsToPage; +function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? ''; + + const optionsList = [ + { + value: 'DEFAULT', + text: translate(`workspace.xero.xeroContactDefault`), + keyForList: 'DEFAULT', + }, + { + value: 'TAGS', + text: 'Tags', + keyForList: 'TAGS', + isSelected: true, + }, + ]; + + return ( + + + + + {translate('workspace.xero.mapXeroRegionsToDescription')} + + {}} + /> + + + ); } XeroMapRegionsToConfigurationPage.displayName = 'XeroMapRegionsToConfigurationPage'; -export default XeroMapRegionsToConfigurationPage; +export default withPolicyConnections(XeroMapRegionsToConfigurationPage); From 7e4f15cb306027c8a459dbe3670e69359e9f30e0 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Thu, 2 May 2024 00:47:32 +0530 Subject: [PATCH 065/219] refactor: update translations --- src/languages/en.ts | 2 +- src/languages/es.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 6350559dc2f7..ee2cc90026eb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1997,7 +1997,7 @@ export default { mapXeroCostCentersTo: 'Map Xero cost centers to', mapXeroRegionsTo: 'Map Xero regions to', mapXeroCostCentersToDescription: 'Choose where to map cost centers to when exporting to Xero.', - mapXeroRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', + mapXeroRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero.', xeroContactDefault: 'Xero contact default', customers: 'Re-bill customers', taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 570038662769..46f1704bbc8b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2020,12 +2020,12 @@ export default { organizationDescription: 'Seleccione la organización en Xero desde la que está importando los datos.', importDescription: 'Elija qué configuraciones de codificación se importan de Xero a Expensify.', trackingCategories: 'Categorías de seguimiento', - trackingCategoriesDescription: 'Choose whether to import tracking categories and see where they are displayed.', - mapXeroCostCentersTo: 'Map Xero cost centers to', - mapXeroRegionsTo: 'Map Xero regions to', - mapXeroCostCentersToDescription: 'Choose where to map cost centers to when exporting to Xero.', - mapXeroRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero', - xeroContactDefault: 'Xero contact default', + trackingCategoriesDescription: 'Elija si desea importar categorías de seguimiento y vea dónde se muestran.', + mapXeroCostCentersTo: 'Asignar centros de costos de Xero a', + mapXeroRegionsTo: 'Asignar regiones de Xero a', + mapXeroCostCentersToDescription: 'Elija dónde asignar los centros de costos al exportar a Xero.', + mapXeroRegionsToDescription: 'Elija dónde asignar las regiones de los empleados al exportar informes de gastos a Xero.', + xeroContactDefault: 'Contacto predeterminado de Xero', customers: 'Volver a facturar a los clientes', taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', notImported: 'No importado', From 121230aa5e7452d77a73a59345aab3bcc945db9d Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 2 May 2024 10:56:50 +0900 Subject: [PATCH 066/219] display Pay Name --- src/languages/en.ts | 2 +- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 48df154a3a5c..17e908113f29 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -609,7 +609,7 @@ export default { splitBill: 'Split expense', splitScan: 'Split receipt', splitDistance: 'Split distance', - sendMoney: 'Pay someone', + paySomeone: ({name}: PaySomeoneParams) => `Pay ${name ?? 'someone'}`, assignTask: 'Assign task', header: 'Quick action', trackManual: 'Track expense', diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index cc61e61aa1f8..fc27432d3e2a 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -34,6 +34,7 @@ import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {getDisplayNameOrDefault} from "@libs/PersonalDetailsUtils"; // On small screen we hide the search page from central pane to show the search bottom tab page with bottom tab bar. // We need to take this in consideration when checking if the screen is focused. @@ -141,7 +142,7 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { case CONST.QUICK_ACTIONS.TRACK_DISTANCE: return 'quickAction.trackDistance'; case CONST.QUICK_ACTIONS.SEND_MONEY: - return 'quickAction.sendMoney'; + return 'quickAction.paySomeone'; case CONST.QUICK_ACTIONS.ASSIGN_TASK: return 'quickAction.assignTask'; default: @@ -191,6 +192,10 @@ function FloatingActionButtonAndPopover( }, [personalDetails, session?.accountID, quickActionReport, quickActionPolicy]); const quickActionTitle = useMemo(() => { + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY) { + const name = getDisplayNameOrDefault(personalDetails?.[session?.accountID]); + return translate('quickAction.paySomeone', {name}) + } const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); return titleKey ? translate(titleKey) : ''; }, [quickAction, translate]); From 1ebb0a6558fda4e0d9e26bb6764b989ba11086dc Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 2 May 2024 11:10:02 +0900 Subject: [PATCH 067/219] hide subtitle when needed --- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index fc27432d3e2a..5aa275a13c0a 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -179,6 +179,14 @@ function FloatingActionButtonAndPopover( const prevIsFocused = usePrevious(isFocused); const {isOffline} = useNetwork(); + const hideQABSubtitle = useMemo(() => { + if (isEmptyObject(quickActionReport)) { + return true; + } + const displayName = personalDetails?.[session?.accountID ?? 0]?.displayName ?? ''; + return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length > 0; + }, [personalDetails, session?.accountID, quickActionReport, quickAction?.action]); + const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection), [allPolicies]); const quickActionAvatars = useMemo(() => { @@ -429,7 +437,7 @@ function FloatingActionButtonAndPopover( isLabelHoverable: false, floatRightAvatars: quickActionAvatars, floatRightAvatarSize: CONST.AVATAR_SIZE.SMALL, - description: !isEmptyObject(quickActionReport) ? ReportUtils.getReportName(quickActionReport) : '', + description: !hideQABSubtitle ? ReportUtils.getReportName(quickActionReport) : '', numberOfLinesDescription: 1, onSelected: () => interceptAnonymousUser(() => navigateToQuickAction()), shouldShowSubscriptRightAvatar: ReportUtils.isPolicyExpenseChat(quickActionReport), From e6f8fde307409b68f90de352f65ab61c68cf3774 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 2 May 2024 11:30:56 +0900 Subject: [PATCH 068/219] use right account --- .../FloatingActionButtonAndPopover.tsx | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 5aa275a13c0a..177593e96dbe 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -179,14 +179,6 @@ function FloatingActionButtonAndPopover( const prevIsFocused = usePrevious(isFocused); const {isOffline} = useNetwork(); - const hideQABSubtitle = useMemo(() => { - if (isEmptyObject(quickActionReport)) { - return true; - } - const displayName = personalDetails?.[session?.accountID ?? 0]?.displayName ?? ''; - return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length > 0; - }, [personalDetails, session?.accountID, quickActionReport, quickAction?.action]); - const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection), [allPolicies]); const quickActionAvatars = useMemo(() => { @@ -200,13 +192,27 @@ function FloatingActionButtonAndPopover( }, [personalDetails, session?.accountID, quickActionReport, quickActionPolicy]); const quickActionTitle = useMemo(() => { - if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY) { - const name = getDisplayNameOrDefault(personalDetails?.[session?.accountID]); + if (isEmptyObject(quickActionReport)) { + return ''; + } + if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { + const name = getDisplayNameOrDefault(personalDetails?.[quickActionAvatars[0]?.id ?? 0]); return translate('quickAction.paySomeone', {name}) } const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); return titleKey ? translate(titleKey) : ''; - }, [quickAction, translate]); + }, [quickAction, translate, quickActionAvatars]); + + const hideQABSubtitle = useMemo(() => { + if (isEmptyObject(quickActionReport)) { + return true; + } + if (quickActionAvatars.length === 0) { + return false; + } + const displayName = personalDetails?.[quickActionAvatars[0]?.id ?? 0]?.firstName ?? ''; + return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; + }, [personalDetails, session?.accountID, quickActionReport, quickAction?.action, quickActionAvatars]); const navigateToQuickAction = () => { switch (quickAction?.action) { From 3c24ca43ad2c81cdcc484d475481c344d5a8864e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 11:14:10 +0800 Subject: [PATCH 069/219] don't set modal visibility when there is another modal still visible --- src/components/Modal/BaseModal.tsx | 8 +++++--- src/libs/actions/Modal.ts | 6 +++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 38701389f78b..89b7afce617d 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -75,9 +75,11 @@ function BaseModal( */ const hideModal = useCallback( (callHideCallback = true) => { - Modal.willAlertModalBecomeVisible(false); - if (shouldSetModalVisibility) { - Modal.setModalVisibility(false); + if (Modal.getModalVisibleCount() === 0) { + Modal.willAlertModalBecomeVisible(false); + if (shouldSetModalVisibility) { + Modal.setModalVisibility(false); + } } if (callHideCallback) { onModalHide(); diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index 7de4548b92c9..5b907655cc3b 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -85,4 +85,8 @@ function willAlertModalBecomeVisible(isVisible: boolean, isPopover = false) { Onyx.merge(ONYXKEYS.MODAL, {willAlertModalBecomeVisible: isVisible, isPopover}); } -export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, setDisableDismissOnEscape, closeTop}; +function getModalVisibleCount() { + return closeModals.length; +} + +export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, setDisableDismissOnEscape, closeTop, getModalVisibleCount}; From 2be83998736ce78903c7fecf098bb2e6da5cf874 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 11:14:18 +0800 Subject: [PATCH 070/219] change fallback route --- src/pages/TransactionReceiptPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/TransactionReceiptPage.tsx b/src/pages/TransactionReceiptPage.tsx index 7a2df49e287a..eff94119ea2b 100644 --- a/src/pages/TransactionReceiptPage.tsx +++ b/src/pages/TransactionReceiptPage.tsx @@ -64,7 +64,7 @@ function TransactionReceipt({transaction, report, reportMetadata = {isLoadingIni originalFileName={receiptURIs?.filename} defaultOpen onModalClose={() => { - Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '')); + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '')); }} isLoading={!transaction && reportMetadata?.isLoadingInitialReportActions} shouldShowNotFoundPage={shouldShowNotFoundPage} From 444ad6c8b2c9a7be681941eda578b68d82c32d65 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 12:07:49 +0800 Subject: [PATCH 071/219] allow member with errors to be shown --- src/libs/PolicyUtils.ts | 14 ++++++++------ src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d24b249df086..d72020f061f7 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -146,17 +146,19 @@ const isPolicyEmployee = (policyID: string, policies: OnyxCollection): b const isPolicyOwner = (policy: OnyxEntry, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID; /** - * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. + * Create an object mapping member emails to their accountIDs. Filter for members without errors if includeMemberWithErrors is false, and get the login email from the personalDetail object using the accountID. * - * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. + * If includeMemberWithErrors is false, We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined): MemberEmailsToAccountIDs { +function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined, includeMemberWithErrors: boolean = false): MemberEmailsToAccountIDs { const members = employeeList ?? {}; const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(members).forEach((email) => { - const member = members?.[email]; - if (Object.keys(member?.errors ?? {})?.length > 0) { - return; + if (!includeMemberWithErrors) { + const member = members?.[email]; + if (Object.keys(member?.errors ?? {})?.length > 0) { + return; + } } const personalDetail = getPersonalDetailByEmail(email); if (!personalDetail?.login) { diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 0d723961a29e..bf4fd9cbde75 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -72,7 +72,7 @@ function invertObject(object: Record): Record { type MemberOption = Omit & {accountID: number}; function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) { - const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList), [policy?.employeeList]); + const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [selectedEmployees, setSelectedEmployees] = useState([]); From bef42435fa0810dd6bc9039447a2f644761bba8c Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 12:08:22 +0800 Subject: [PATCH 072/219] fix wrong login is used for optimistic member --- src/libs/actions/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 547b9a203375..645e71ab4a79 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1411,7 +1411,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount const optimisticMembersState: OnyxCollection = {}; const successMembersState: OnyxCollection = {}; const failureMembersState: OnyxCollection = {}; - Object.keys(invitedEmailsToAccountIDs).forEach((email) => { + logins.forEach((email) => { optimisticMembersState[email] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.POLICY.ROLE.USER}; successMembersState[email] = {pendingAction: null}; failureMembersState[email] = { From f226ba4ddc57bf1782f30144c3be2313ad8f34a2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 12:08:35 +0800 Subject: [PATCH 073/219] fix can't clear errors --- src/libs/actions/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 645e71ab4a79..f357557ff26d 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -2621,7 +2621,7 @@ function setWorkspaceInviteMessageDraft(policyID: string, message: string | null } function clearErrors(policyID: string) { - setWorkspaceErrors(policyID, {}); + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errors: null}); hideWorkspaceAlertMessage(policyID); } From 9960b556c6baafec3a36817a1b8b74ede52be125 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 12:09:02 +0800 Subject: [PATCH 074/219] only delete the workspace if it's an invalid or pending add policy --- src/pages/workspace/WorkspaceInitialPage.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 1c861d510a85..a1e5a572f1e6 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -34,6 +34,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -75,9 +76,13 @@ type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & Workspace type PolicyFeatureStates = Record; -function dismissError(policyID: string) { - PolicyUtils.goBackFromInvalidPolicy(); - Policy.removeWorkspace(policyID); +function dismissError(policyID: string, pendingAction: PendingAction | undefined) { + if (!policyID || pendingAction == CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + PolicyUtils.goBackFromInvalidPolicy(); + Policy.removeWorkspace(policyID); + } else { + Policy.clearErrors(policyID); + } } function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAccount, policyCategories}: WorkspaceInitialPageProps) { @@ -340,7 +345,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc dismissError(policyID)} + onClose={() => dismissError(policyID, policy?.pendingAction)} errors={policy?.errors} errorRowStyles={[styles.ph5, styles.pv2]} shouldDisableStrikeThrough={false} From b10bfb6943f031fd1b6f5a85d356e8949df8ef0b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 12:09:15 +0800 Subject: [PATCH 075/219] fix can't dismiss error --- src/pages/workspace/WorkspacesListPage.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index a22a3679b435..60e471deb328 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -102,7 +102,7 @@ const workspaceFeatures: FeatureListItem[] = [ /** * Dismisses the errors on one item */ -function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.PendingAction) { +function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.PendingAction | undefined) { if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { Policy.clearDeleteWorkspaceError(policyID); return; @@ -112,7 +112,8 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi Policy.removeWorkspace(policyID); return; } - throw new Error('Not implemented'); + + Policy.clearErrors(policyID); } const stickyHeaderIndices = [0]; @@ -339,12 +340,7 @@ function WorkspacesListPage({policies, reimbursementAccount, reports, session}: brickRoadIndicator: reimbursementAccountBrickRoadIndicator ?? PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy), pendingAction: policy.pendingAction, errors: policy.errors, - dismissError: () => { - if (!policy.pendingAction) { - return; - } - dismissWorkspaceError(policy.id, policy.pendingAction); - }, + dismissError: () => dismissWorkspaceError(policy.id, policy.pendingAction), disabled: policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, iconType: policy.avatarURL ? CONST.ICON_TYPE_AVATAR : CONST.ICON_TYPE_ICON, iconFill: theme.textLight, From cbef05c8e92ea3540df14615d26853f7513c0f09 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 2 May 2024 12:22:18 +0800 Subject: [PATCH 076/219] lint --- src/libs/PolicyUtils.ts | 2 +- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d72020f061f7..51221ddb1236 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -150,7 +150,7 @@ const isPolicyOwner = (policy: OnyxEntry, currentUserAccountID: number): * * If includeMemberWithErrors is false, We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined, includeMemberWithErrors: boolean = false): MemberEmailsToAccountIDs { +function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined, includeMemberWithErrors = false): MemberEmailsToAccountIDs { const members = employeeList ?? {}; const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(members).forEach((email) => { diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index a1e5a572f1e6..88532856465a 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -77,7 +77,7 @@ type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & Workspace type PolicyFeatureStates = Record; function dismissError(policyID: string, pendingAction: PendingAction | undefined) { - if (!policyID || pendingAction == CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + if (!policyID || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { PolicyUtils.goBackFromInvalidPolicy(); Policy.removeWorkspace(policyID); } else { From ec0c3dfb63163718cdd95290aec9a20776f228de Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 2 May 2024 15:30:03 +0700 Subject: [PATCH 077/219] Add final solution --- src/libs/actions/Report.ts | 20 ++++++++++++------- .../ComposerWithSuggestions.tsx | 16 ++++++++++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 58ea252c3c61..60e6682c5fdd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -104,7 +104,6 @@ import * as CachedPDFPaths from './CachedPDFPaths'; import * as Modal from './Modal'; import * as Session from './Session'; import * as Welcome from './Welcome'; -import getDraftComment from '@libs/ComposerUtils/getDraftComment'; type SubscriberCallback = (isFromCurrentUser: boolean, reportActionID: string | undefined) => void; @@ -1175,6 +1174,10 @@ function saveReportDraftComment(reportID: string, comment: string | null) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)); } +function saveReportDraftCommentWithCallback(reportID: string, comment: string | null, callback: () => void) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)).then(callback); +} + /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ function broadcastUserIsTyping(reportID: string) { const privateReportChannelName = getReportChannelName(reportID); @@ -1203,15 +1206,17 @@ function handleReportChanged(report: OnyxEntry) { // In this case, the API will let us know by returning a preexistingReportID. // We should clear out the optimistically created report and re-route the user to the preexisting report. if (report?.reportID && report.preexistingReportID) { - const draftComment = getDraftComment(report.reportID); - saveReportComment(report.preexistingReportID, draftComment ?? ''); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null); - + let callback = () => {}; // Only re-route them if they are still looking at the optimistically created report if (Navigation.getActiveRoute().includes(`/r/${report.reportID}`)) { - // Pass 'FORCED_UP' type to replace new report on second login with proper one in the Navigation - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID), CONST.NAVIGATION.TYPE.FORCED_UP); + callback = () => { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? ''), CONST.NAVIGATION.TYPE.FORCED_UP); + }; } + DeviceEventEmitter.emit(`switchToCurrentReport_${report.reportID}`, { + preexistingReportID: report.preexistingReportID, + callback, + }); return; } @@ -3712,4 +3717,5 @@ export { leaveGroupChat, removeFromGroupChat, updateGroupChatMemberRoles, + saveReportDraftCommentWithCallback, }; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 469a7300a84f..fe827ecaa69c 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -12,7 +12,7 @@ import type { TextInputKeyPressEventData, TextInputSelectionChangeEventData, } from 'react-native'; -import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; +import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {useAnimatedRef} from 'react-native-reanimated'; @@ -344,6 +344,20 @@ function ComposerWithSuggestions( [], ); + useEffect(() => { + const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToCurrentReport_${reportID}`, ({preexistingReportID, callback}) => { + if (!commentRef.current) { + callback(); + return; + } + Report.saveReportDraftCommentWithCallback(preexistingReportID, commentRef.current, callback); + }); + + return () => { + switchToCurrentReport.remove(); + }; + }, [reportID]); + /** * Find the newly added characters between the previous text and the new text based on the selection. * From fc2de8c88b2d91a538c2fe8c0c4807ba8aa06ca2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 2 May 2024 16:52:35 +0700 Subject: [PATCH 078/219] fix add fail test case --- tests/actions/PolicyTaxTest.ts | 482 +++++++++++++++++++++++++++++---- 1 file changed, 432 insertions(+), 50 deletions(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index 3899e0c2a24e..c35aea14e4f8 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -11,6 +11,7 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; OnyxUpdateManager(); describe('actions/PolicyTax', () => { + const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -20,22 +21,21 @@ describe('actions/PolicyTax', () => { beforeEach(() => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); - return Onyx.clear().then(waitForBatchedUpdates); + return Onyx.clear() + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy)) + .then(waitForBatchedUpdates); }); describe('SetPolicyCustomTaxName', () => { it('Set policy`s custom tax name', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const customTaxName = 'Custom tag name'; + const originalCustomTaxName = fakePolicy?.taxRates?.name; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -72,20 +72,65 @@ describe('actions/PolicyTax', () => { ) ); }); + it('Reset policy`s custom tax name when API returns an error', () => { + const customTaxName = 'Custom tag name'; + const originalCustomTaxName = fakePolicy?.taxRates?.name; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(customTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.name).toBe(originalCustomTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.name).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); + describe('SetPolicyCurrencyDefaultTax', () => { it('Set policy`s currency default tax', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxCode = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -122,20 +167,64 @@ describe('actions/PolicyTax', () => { ) ); }); + it('Reset policy`s currency default tax when API returns an error', () => { + const taxCode = 'id_TAX_RATE_1'; + const originalDefaultExternalID = fakePolicy?.taxRates?.defaultExternalID; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.defaultExternalID).toBe(originalDefaultExternalID); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.defaultExternalID).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); describe('SetPolicyForeignCurrencyDefaultTax', () => { it('Set policy`s foreign currency default', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxCode = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -173,10 +262,59 @@ describe('actions/PolicyTax', () => { ) ); }); + it('Reset policy`s foreign currency default when API returns an error', () => { + const taxCode = 'id_TAX_RATE_1'; + const originalDefaultForeignCurrencyID = fakePolicy?.taxRates?.foreignTaxDefault; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.taxRates?.foreignTaxDefault).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if the policy pendingFields was cleared + expect(policy?.taxRates?.foreignTaxDefault).toBe(originalDefaultForeignCurrencyID); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(policy?.taxRates?.errorFields?.foreignTaxDefault).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); describe('CreatePolicyTax', () => { it('Create a new tax', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const newTaxRate: TaxRate = { name: 'Tax rate 2', value: '2%', @@ -185,12 +323,9 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + createPolicyTax(fakePolicy.id, newTaxRate); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - createPolicyTax(fakePolicy.id, newTaxRate); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -230,19 +365,68 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Remove the optimistic tax if the API returns an error', () => { + const newTaxRate: TaxRate = { + name: 'Tax rate 2', + value: '2%', + code: 'id_TAX_RATE_2', + }; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + createPolicyTax(fakePolicy.id, newTaxRate); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.code).toBe(newTaxRate.code); + expect(createdTax?.name).toBe(newTaxRate.name); + expect(createdTax?.value).toBe(newTaxRate.value); + expect(createdTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.errors).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); describe('SetPolicyTaxesEnabled', () => { it('Disable policy`s taxes', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const disableTaxID = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -282,21 +466,68 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Disable policy`s taxes but API returns an error, then enable policy`s taxes again', () => { + const disableTaxID = 'id_TAX_RATE_1'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); + const originalTaxes = {...fakePolicy?.taxRates?.taxes}; + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBe(!!originalTaxes[disableTaxID].isDisabled); + expect(disabledTax?.errorFields?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); }); describe('RenamePolicyTax', () => { it('Rename tax', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxID = 'id_TAX_RATE_1'; const newTaxName = 'Tax rate 1 updated'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + renamePolicyTax(fakePolicy.id, taxID, newTaxName); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - renamePolicyTax(fakePolicy.id, taxID, newTaxName); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -336,21 +567,69 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Rename tax but API returns an error, then recover the original tax`s name', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxName = 'Tax rate 1 updated'; + const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + renamePolicyTax(fakePolicy.id, taxID, newTaxName); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(newTaxName); + expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(originalTaxRate.name); + expect(updatedTax?.errorFields?.name).toBeTruthy(); + expect(updatedTax?.pendingFields?.name).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); }); describe('UpdatePolicyTaxValue', () => { it('Update tax`s value', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const taxID = 'id_TAX_RATE_1'; const newTaxValue = 10; const stringTaxValue = `${newTaxValue}%`; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -390,21 +669,70 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Update tax`s value but API returns an error, then recover the original tax`s value', () => { + const taxID = 'id_TAX_RATE_1'; + const newTaxValue = 10; + const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; + const stringTaxValue = `${newTaxValue}%`; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(stringTaxValue); + expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.value).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(originalTaxRate.value); + expect(updatedTax?.errorFields?.value).toBeTruthy(); + expect(updatedTax?.pendingFields?.value).toBeFalsy(); + resolve(); + }, + }); + }), + ); + }); }); describe('DeletePolicyTaxes', () => { it('Delete tax that is not foreignTaxDefault', () => { - const fakePolicy: PolicyType = {...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX}; const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; const taxID = 'id_TAX_RATE_1'; // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); + deletePolicyTaxes(fakePolicy.id, [taxID]); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) - .then(() => { - deletePolicyTaxes(fakePolicy.id, [taxID]); - return waitForBatchedUpdates(); - }) + waitForBatchedUpdates() .then( () => new Promise((resolve) => { @@ -461,7 +789,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); return ( - Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, {taxRates: {foreignTaxDefault: 'id_TAX_RATE_1'}}) .then(() => { deletePolicyTaxes(fakePolicy.id, [taxID]); return waitForBatchedUpdates(); @@ -507,5 +835,59 @@ describe('actions/PolicyTax', () => { ) ); }); + + it('Delete tax that is not foreignTaxDefault but API return an error, then recover the delated tax', () => { + const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; + const taxID = 'id_TAX_RATE_1'; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume(); + }) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax?.pendingAction).toBeFalsy(); + expect(deletedTax?.errors).toBeTruthy(); + resolve(); + }, + }); + }), + ); + }); }); }); From c9ad9aa80a601f59090ea5bf767dfad678c9bd6e Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 2 May 2024 17:02:43 +0700 Subject: [PATCH 079/219] fix lint --- tests/actions/PolicyTaxTest.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index c35aea14e4f8..3341fe714639 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -29,8 +29,6 @@ describe('actions/PolicyTax', () => { describe('SetPolicyCustomTaxName', () => { it('Set policy`s custom tax name', () => { const customTaxName = 'Custom tag name'; - const originalCustomTaxName = fakePolicy?.taxRates?.name; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.pause(); Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); @@ -776,13 +774,6 @@ describe('actions/PolicyTax', () => { }); it('Delete tax that is foreignTaxDefault', () => { - const fakePolicy: PolicyType = { - ...createRandomPolicy(0), - taxRates: { - ...CONST.DEFAULT_TAX, - foreignTaxDefault: 'id_TAX_RATE_1', - }, - }; const taxID = 'id_TAX_RATE_1'; const firstTaxID = 'id_TAX_EXEMPT'; From e37e0b7827cf3ec606c7c4b6a21de34ee18fba4d Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 2 May 2024 17:06:38 +0700 Subject: [PATCH 080/219] fix lint --- tests/actions/PolicyTaxTest.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index 3341fe714639..a17179d8f7af 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -98,7 +98,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -193,7 +193,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -289,7 +289,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -397,7 +397,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -494,7 +494,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -596,7 +596,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -699,7 +699,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( @@ -858,7 +858,7 @@ describe('actions/PolicyTax', () => { // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. fetch.fail(); // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume(); + return fetch.resume() as Promise; }) .then(waitForBatchedUpdates) .then( From d20d27d1d6678f5e8cc45bb74dd51ea3710517c2 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Thu, 2 May 2024 22:45:15 +0530 Subject: [PATCH 081/219] fix: added fields --- src/CONST.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 630dabc404f2..b7ce000b524f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1270,6 +1270,10 @@ const CONST = { IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', IMPORT_TRACK_CATEGORIES: 'importTrackingCategories', + CATEGORY_FIELDS: { + COST_CENTERS: 'cost centers', + REGION: 'region', + } }, QUICKBOOKS_OUT_OF_POCKET_EXPENSE_ACCOUNT_TYPE: { From f1bd8bd9adc45fb5d66a22b03dc50adbeb33694e Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Fri, 3 May 2024 07:53:19 +0700 Subject: [PATCH 082/219] 40211 handle conflict mapview event on android --- .../DraggableList/index.android.tsx | 30 +++++++++++++++++++ .../{index.native.tsx => index.ios.tsx} | 0 .../MapView/responder/index.android.ts | 11 +++++++ 3 files changed, 41 insertions(+) create mode 100644 src/components/DraggableList/index.android.tsx rename src/components/DraggableList/{index.native.tsx => index.ios.tsx} (100%) create mode 100644 src/components/MapView/responder/index.android.ts diff --git a/src/components/DraggableList/index.android.tsx b/src/components/DraggableList/index.android.tsx new file mode 100644 index 000000000000..7d1add9d3da1 --- /dev/null +++ b/src/components/DraggableList/index.android.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import DraggableFlatList from 'react-native-draggable-flatlist'; +import type {FlatList} from 'react-native-gesture-handler'; +import useThemeStyles from '@hooks/useThemeStyles'; +import { View } from 'react-native'; +import type {DraggableListProps} from './types'; + +function DraggableList({renderClone, shouldUsePortal, ListFooterComponent, ...viewProps}: DraggableListProps, ref: React.ForwardedRef>) { + const styles = useThemeStyles(); + return ( + + + {ListFooterComponent && ( + + {ListFooterComponent} + + )} + + ); +} + +DraggableList.displayName = 'DraggableList'; + +export default React.forwardRef(DraggableList); diff --git a/src/components/DraggableList/index.native.tsx b/src/components/DraggableList/index.ios.tsx similarity index 100% rename from src/components/DraggableList/index.native.tsx rename to src/components/DraggableList/index.ios.tsx diff --git a/src/components/MapView/responder/index.android.ts b/src/components/MapView/responder/index.android.ts new file mode 100644 index 000000000000..e1907e786733 --- /dev/null +++ b/src/components/MapView/responder/index.android.ts @@ -0,0 +1,11 @@ +import { PanResponder } from 'react-native'; + +const InterceptPanResponderCapture = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => false, +}); + +export default InterceptPanResponderCapture; From 9a125955aa29beaaacb550391c48e4e351c5b1c0 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 3 May 2024 09:44:46 +0700 Subject: [PATCH 083/219] hide delete option for multilevel tags --- src/libs/PolicyUtils.ts | 8 ++++++++ src/pages/workspace/tags/TagSettingsPage.tsx | 3 ++- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 15 +++++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d24b249df086..29965f188932 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -248,6 +248,13 @@ function getCountOfEnabledTagsOfList(policyTags: PolicyTags) { return Object.values(policyTags).filter((policyTag) => policyTag.enabled).length; } +/** + * Whether the policy has multi-level tags + */ +function isMultiLevelTags(policyTagList: OnyxEntry): boolean { + return Object.keys(policyTagList ?? {}).length > 1; +} + function isPendingDeletePolicy(policy: OnyxEntry): boolean { return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } @@ -430,6 +437,7 @@ export { getTagList, getCleanedTagName, getCountOfEnabledTagsOfList, + isMultiLevelTags, isPendingDeletePolicy, isPolicyEmployee, isPolicyOwner, diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index ed88a1a9b636..3499afd3bc03 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -39,6 +39,7 @@ function TagSettingsPage({route, policyTags}: TagSettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const policyTag = useMemo(() => PolicyUtils.getTagList(policyTags, 0), [policyTags]); + const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags); const {windowWidth} = useWindowDimensions(); @@ -77,7 +78,7 @@ function TagSettingsPage({route, policyTags}: TagSettingsPageProps) { > PolicyUtils.getTagLists(policyTags).slice(0, 1), [policyTags]); + const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags); const tagList = useMemo( () => policyTagLists @@ -164,12 +165,14 @@ function WorkspaceTagsPage({route, policy}: WorkspaceTagsPageProps) { const options: Array>> = []; if (selectedTagsArray.length > 0) { - options.push({ - icon: Expensicons.Trashcan, - text: translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags'), - value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DELETE, - onSelected: () => setDeleteTagsConfirmModalVisible(true), - }); + if (!isMultiLevelTags) { + options.push({ + icon: Expensicons.Trashcan, + text: translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags'), + value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DELETE, + onSelected: () => setDeleteTagsConfirmModalVisible(true), + }); + } const enabledTags = selectedTagsArray.filter((tagName) => tagListKeyedByName?.[tagName]?.enabled); if (enabledTags.length > 0) { From 9bf90d6286646e08ba6f521213aacf7eef677aa3 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Fri, 3 May 2024 08:30:56 +0530 Subject: [PATCH 084/219] feat: added tracking categories logic --- src/CONST.ts | 3 +- .../ConnectToXeroButton/index.native.tsx | 2 +- src/components/ConnectToXeroButton/index.tsx | 2 +- src/languages/en.ts | 5 ++- src/languages/es.ts | 13 ++++--- src/libs/actions/connections/ConnectToXero.ts | 17 ++++++++- .../XeroTrackingCategoryConfigurationPage.tsx | 35 ++++++++++++++----- src/types/onyx/Policy.ts | 17 ++++++--- 8 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 827c6f9fce88..cd06c295959c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1271,7 +1271,8 @@ const CONST = { IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', IMPORT_TRACK_CATEGORIES: 'importTrackingCategories', - CATEGORY_FIELDS: { + TRACK_CATEGORY_PREFIX: 'trackingCategory_', + TRACK_CATEGORY_FIELDS: { COST_CENTERS: 'cost centers', REGION: 'region', } diff --git a/src/components/ConnectToXeroButton/index.native.tsx b/src/components/ConnectToXeroButton/index.native.tsx index 36c5af4a0575..f5e819136f00 100644 --- a/src/components/ConnectToXeroButton/index.native.tsx +++ b/src/components/ConnectToXeroButton/index.native.tsx @@ -11,7 +11,7 @@ import Modal from '@components/Modal'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; -import getXeroSetupLink from '@libs/actions/connections/ConnectToXero'; +import {getXeroSetupLink} from '@libs/actions/connections/ConnectToXero'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Session} from '@src/types/onyx'; diff --git a/src/components/ConnectToXeroButton/index.tsx b/src/components/ConnectToXeroButton/index.tsx index 8fad63e1a965..318c3bac4e2a 100644 --- a/src/components/ConnectToXeroButton/index.tsx +++ b/src/components/ConnectToXeroButton/index.tsx @@ -5,7 +5,7 @@ import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; -import getXeroSetupLink from '@libs/actions/connections/ConnectToXero'; +import {getXeroSetupLink} from '@libs/actions/connections/ConnectToXero'; import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; import type {ConnectToXeroButtonProps} from './types'; diff --git a/src/languages/en.ts b/src/languages/en.ts index 27452bcd3e76..87a3bdaf2a50 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2004,11 +2004,14 @@ export default { mapXeroRegionsTo: 'Map Xero regions to', mapXeroCostCentersToDescription: 'Choose where to map cost centers to when exporting to Xero.', mapXeroRegionsToDescription: 'Choose where to map employee regions when exporting expense reports to Xero.', - xeroContactDefault: 'Xero contact default', customers: 'Re-bill customers', customersDescription: 'Import customer contacts. Billable expenses need tags for export. Expenses will carry the customer information to Xero for sales invoices.', taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', notImported: 'Not imported', + trackingCategoriesOptions: { + default: 'Xero contact default', + tag: 'Tags' + }, }, type: { free: 'Free', diff --git a/src/languages/es.ts b/src/languages/es.ts index bc3117b6be04..a93af09bfbe1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2031,17 +2031,20 @@ export default { organizationDescription: 'Seleccione la organización en Xero desde la que está importando los datos.', importDescription: 'Elija qué configuraciones de codificación se importan de Xero a Expensify.', trackingCategories: 'Categorías de seguimiento', - trackingCategoriesDescription: 'Elija si desea importar categorías de seguimiento y vea dónde se muestran.', - mapXeroCostCentersTo: 'Asignar centros de costos de Xero a', + trackingCategoriesDescription: 'Elige si deseas importar categorías de seguimiento y ver dónde se muestran.', + mapXeroCostCentersTo: 'Asignar centros de coste de Xero a', mapXeroRegionsTo: 'Asignar regiones de Xero a', - mapXeroCostCentersToDescription: 'Elija dónde asignar los centros de costos al exportar a Xero.', - mapXeroRegionsToDescription: 'Elija dónde asignar las regiones de los empleados al exportar informes de gastos a Xero.', - xeroContactDefault: 'Contacto predeterminado de Xero', + mapXeroCostCentersToDescription: 'Elige dónde mapear los centros de coste al exportar a Xero.', + mapXeroRegionsToDescription: 'Elige dónde asignar las regiones de los empleados al exportar informes de gastos a Xero.', customers: 'Volver a facturar a los clientes', customersDescription: 'Importar contactos de clientes. Los gastos facturables necesitan etiquetas para la exportación. Los gastos llevarán la información del cliente a Xero para las facturas de ventas.', taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', notImported: 'No importado', + trackingCategoriesOptions: { + default: 'Contacto de Xero por defecto', + tag: 'Etiquetas' + }, }, type: { free: 'Gratis', diff --git a/src/libs/actions/connections/ConnectToXero.ts b/src/libs/actions/connections/ConnectToXero.ts index b5e8d7ab3298..99af19a1b0ab 100644 --- a/src/libs/actions/connections/ConnectToXero.ts +++ b/src/libs/actions/connections/ConnectToXero.ts @@ -1,6 +1,9 @@ import type {ConnectPolicyToAccountingIntegrationParams} from '@libs/API/parameters'; import {READ_COMMANDS} from '@libs/API/types'; import {getCommandURL} from '@libs/ApiUtils'; +import CONST from '@src/CONST'; +import type {OnyxEntry} from 'react-native-onyx'; +import type * as OnyxTypes from '@src/types/onyx'; const getXeroSetupLink = (policyID: string) => { const params: ConnectPolicyToAccountingIntegrationParams = {policyID}; @@ -8,4 +11,16 @@ const getXeroSetupLink = (policyID: string) => { return commandURL + new URLSearchParams(params).toString(); }; -export default getXeroSetupLink; +const getTrackingCategoryValue = (policy: OnyxEntry, key: string): string => { + const { trackingCategories } = policy?.connections?.xero?.data ?? {}; + const { mappings } = policy?.connections?.xero?.config ?? {}; + + const category = trackingCategories?.find((category) => category.name.toLowerCase() === key.toLowerCase()); + if (!category) { + return ""; + } + + return mappings?.[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`] ?? ""; +} + +export {getXeroSetupLink, getTrackingCategoryValue}; diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 3ee0870c2c60..83df03212d98 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -16,24 +16,40 @@ import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import { getTrackingCategoryValue } from '@libs/actions/connections/ConnectToXero'; +import { TranslationPaths } from '@src/languages/types'; function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; const {importTrackingCategories, pendingFields} = policy?.connections?.xero?.config ?? {}; + const { trackingCategories } = policy?.connections?.xero?.data ?? {}; + const menuItems = useMemo( - () => [ - { - title: translate('workspace.xero.mapXeroCostCentersTo'), + () => { + const availableCategories = []; + + const costCenterCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + const regionCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + if (costCenterCategoryValue) { + availableCategories.push({ + description: translate('workspace.xero.mapXeroCostCentersTo'), action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS.getRoute(policyID)), - }, - { - title: translate('workspace.xero.mapXeroRegionsTo'), + title: translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths) + }); + } + + if (trackingCategories?.find((category) => category.name.toLowerCase() === CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION)) { + availableCategories.push({ + description: translate('workspace.xero.mapXeroRegionsTo'), action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_REGIONS.getRoute(policyID)), - }, - ], - [translate, policyID], + title: translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths) + }); + } + return availableCategories; + }, + [translate, policyID, trackingCategories], ); return ( @@ -78,6 +94,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index d64de6196985..3655c233cbef 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -207,6 +207,11 @@ type Tenant = { value: string; }; +type XeroTrackingCategory = { + id: string; + name: string; +} + type XeroConnectionData = { bankAccounts: unknown[]; countryCode: string; @@ -216,7 +221,13 @@ type XeroConnectionData = { name: string; }>; tenants: Tenant[]; - trackingCategories: unknown[]; + trackingCategories: XeroTrackingCategory[]; +}; + +type XeroMappingType = { + customer: string; +} & { + [key in `trackingCategory_${string}`]: string; }; /** @@ -244,9 +255,7 @@ type XeroConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ importTaxRates: boolean; importTrackingCategories: boolean; isConfigured: boolean; - mappings: { - customer: string; - }; + mappings: XeroMappingType; sync: { hasChosenAutoSyncOption: boolean; hasChosenSyncReimbursedReportsOption: boolean; From 0872f9cf77363fd0962a1b39850f16fc30ba2edb Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Fri, 3 May 2024 09:14:15 +0530 Subject: [PATCH 085/219] feat: create views for cost centers and region --- src/CONST.ts | 4 +++ .../XeroMapCostCentersToConfigurationPage.tsx | 29 +++++++++--------- .../XeroMapRegionsToConfigurationPage.tsx | 30 ++++++++++--------- 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index cd06c295959c..66b2ca8d6eb3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1275,6 +1275,10 @@ const CONST = { TRACK_CATEGORY_FIELDS: { COST_CENTERS: 'cost centers', REGION: 'region', + }, + TRACK_CATEGORY_OPTIONS: { + DEFAULT: 'DEFAULT', + TAG: 'TAG' } }, diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 56557fdd4d13..b6444f3eeb9a 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -10,26 +10,27 @@ import useThemeStyles from '@hooks/useThemeStyles'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import { getTrackingCategoryValue } from '@libs/actions/connections/ConnectToXero'; import CONST from '@src/CONST'; +import { TranslationPaths } from '@src/languages/types'; function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; - const optionsList = [ - { - value: 'DEFAULT', - text: translate(`workspace.xero.xeroContactDefault`), - keyForList: 'DEFAULT', - }, - { - value: 'TAGS', - text: 'Tags', - keyForList: 'TAGS', - isSelected: true, - }, - ]; + const optionsList = useMemo(() => { + const costCenterCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + + return Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => { + return { + value: option, + text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), + keyForList: option, + isSelected: option.toLowerCase() === costCenterCategoryValue.toLowerCase() + } + }); + }, [policyID, translate]); return ( { + const costCenterCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION); + + return Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => { + return { + value: option, + text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), + keyForList: option, + isSelected: option.toLowerCase() === costCenterCategoryValue.toLowerCase() + } + }); + }, [policyID, translate]); return ( Date: Fri, 3 May 2024 09:30:05 +0530 Subject: [PATCH 086/219] refactor: update category fetching logic --- src/libs/actions/connections/ConnectToXero.ts | 11 +++++++---- .../xero/XeroMapCostCentersToConfigurationPage.tsx | 8 +++++--- .../xero/XeroMapRegionsToConfigurationPage.tsx | 4 ++-- .../xero/XeroTrackingCategoryConfigurationPage.tsx | 6 +++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/connections/ConnectToXero.ts b/src/libs/actions/connections/ConnectToXero.ts index 99af19a1b0ab..11aa904a678a 100644 --- a/src/libs/actions/connections/ConnectToXero.ts +++ b/src/libs/actions/connections/ConnectToXero.ts @@ -11,16 +11,19 @@ const getXeroSetupLink = (policyID: string) => { return commandURL + new URLSearchParams(params).toString(); }; -const getTrackingCategoryValue = (policy: OnyxEntry, key: string): string => { +const getTrackingCategory = (policy: OnyxEntry, key: string) => { const { trackingCategories } = policy?.connections?.xero?.data ?? {}; const { mappings } = policy?.connections?.xero?.config ?? {}; const category = trackingCategories?.find((category) => category.name.toLowerCase() === key.toLowerCase()); if (!category) { - return ""; + return undefined; } - return mappings?.[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`] ?? ""; + return { + ...category, + value: mappings?.[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`] ?? "" + }; } -export {getXeroSetupLink, getTrackingCategoryValue}; +export {getXeroSetupLink, getTrackingCategory}; diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index b6444f3eeb9a..a7720e5c8ef5 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -10,7 +10,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; -import { getTrackingCategoryValue } from '@libs/actions/connections/ConnectToXero'; +import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import CONST from '@src/CONST'; import { TranslationPaths } from '@src/languages/types'; @@ -19,15 +19,17 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id ?? ''; + const costCenterCategory = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + const optionsList = useMemo(() => { - const costCenterCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + return Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => { return { value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), keyForList: option, - isSelected: option.toLowerCase() === costCenterCategoryValue.toLowerCase() + isSelected: option.toLowerCase() === costCenterCategory?.value?.toLowerCase() } }); }, [policyID, translate]); diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 5ccde7150d22..24d04be14804 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -11,7 +11,7 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; -import { getTrackingCategoryValue } from '@libs/actions/connections/ConnectToXero'; +import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import { TranslationPaths } from '@src/languages/types'; @@ -21,7 +21,7 @@ function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { const policyID = policy?.id ?? ''; const optionsList = useMemo(() => { - const costCenterCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION); + const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION)?.value ?? ""; return Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => { return { diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 83df03212d98..55a2e2b5538c 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -16,7 +16,7 @@ import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import { getTrackingCategoryValue } from '@libs/actions/connections/ConnectToXero'; +import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import { TranslationPaths } from '@src/languages/types'; function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { @@ -30,8 +30,8 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { () => { const availableCategories = []; - const costCenterCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); - const regionCategoryValue = getTrackingCategoryValue(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS)?.value ?? ""; + const regionCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION)?.value ?? ""; if (costCenterCategoryValue) { availableCategories.push({ description: translate('workspace.xero.mapXeroCostCentersTo'), From a4cc8971a93f0d05c889bfc9d9a5f9c3bcce2fa1 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 3 May 2024 11:37:53 +0700 Subject: [PATCH 087/219] Display error when deleting receipt failure --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/IOU.ts | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index a81589f634a3..1e327edf99c3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -714,6 +714,7 @@ export default { other: 'Unexpected error, please try again later.', genericCreateFailureMessage: 'Unexpected error submitting this expense. Please try again later.', genericCreateInvoiceFailureMessage: 'Unexpected error sending invoice, please try again later.', + receiptDeleteFailureError: 'Unexpected error deleting this receipt. Please try again later.', // eslint-disable-next-line rulesdir/use-periods-for-error-messages receiptFailureMessage: "The receipt didn't upload. ", // eslint-disable-next-line rulesdir/use-periods-for-error-messages diff --git a/src/languages/es.ts b/src/languages/es.ts index cbedd0c555a4..4e5257f3bf8e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -709,6 +709,7 @@ export default { other: 'Error inesperado, por favor inténtalo más tarde.', genericCreateFailureMessage: 'Error inesperado al enviar este gasto. Por favor, inténtalo más tarde.', genericCreateInvoiceFailureMessage: 'Error inesperado al enviar la factura, inténtalo de nuevo más tarde.', + receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Vuelva a intentarlo más tarde.', // eslint-disable-next-line rulesdir/use-periods-for-error-messages receiptFailureMessage: 'El recibo no se subió. ', // eslint-disable-next-line rulesdir/use-periods-for-error-messages diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e0b406ad9c45..1a3391fa6d18 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -6250,7 +6250,10 @@ function detachReceipt(transactionID: string) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: transaction, + value: { + ...transaction, + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.receiptDeleteFailureError'), + }, }, ]; From f1e8dbc515f1723588733312ec8fbdeff65aac4f Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Fri, 3 May 2024 10:18:29 +0530 Subject: [PATCH 088/219] feat: update mapping --- .../XeroMapCostCentersToConfigurationPage.tsx | 17 +++++++++++++---- .../XeroTrackingCategoryConfigurationPage.tsx | 15 ++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index a7720e5c8ef5..4bb9af400440 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -7,19 +7,21 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import CONST from '@src/CONST'; import { TranslationPaths } from '@src/languages/types'; +import Navigation from '@libs/Navigation/Navigation'; function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; - const costCenterCategory = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); const optionsList = useMemo(() => { @@ -29,15 +31,16 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), keyForList: option, - isSelected: option.toLowerCase() === costCenterCategory?.value?.toLowerCase() + isSelected: option.toLowerCase() === category?.value?.toLowerCase() } }); }, [policyID, translate]); + return ( {}} + onSelectRow={(row) => { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`]: row.value}: {}) + }) + Navigation.goBack(); + }} /> diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 55a2e2b5538c..e68e6d94a266 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -18,6 +18,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import { TranslationPaths } from '@src/languages/types'; +import { MenuItemProps } from '@components/MenuItem'; function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); @@ -26,7 +27,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const {importTrackingCategories, pendingFields} = policy?.connections?.xero?.config ?? {}; const { trackingCategories } = policy?.connections?.xero?.data ?? {}; - const menuItems = useMemo( + const menuItems: MenuItemProps[] = useMemo( () => { const availableCategories = []; @@ -35,7 +36,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { if (costCenterCategoryValue) { availableCategories.push({ description: translate('workspace.xero.mapXeroCostCentersTo'), - action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS.getRoute(policyID)), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS.getRoute(policyID)), title: translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths) }); } @@ -43,13 +44,13 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { if (trackingCategories?.find((category) => category.name.toLowerCase() === CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION)) { availableCategories.push({ description: translate('workspace.xero.mapXeroRegionsTo'), - action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_REGIONS.getRoute(policyID)), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_REGIONS.getRoute(policyID)), title: translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths) }); } return availableCategories; }, - [translate, policyID, trackingCategories], + [translate, policy, policyID, trackingCategories], ); return ( @@ -90,13 +91,13 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) {
{importTrackingCategories && ( - {menuItems.map((menuItem) => ( + {menuItems.map((menuItem: MenuItemProps) => ( ))} From b24e8757e31e0b7779d6c8ad08abe5585f63d2ee Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Fri, 3 May 2024 11:00:06 +0530 Subject: [PATCH 089/219] feat: added api for regions --- src/CONST.ts | 1 + .../XeroMapCostCentersToConfigurationPage.tsx | 7 ++++--- .../xero/XeroMapRegionsToConfigurationPage.tsx | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 66b2ca8d6eb3..2522632df677 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1271,6 +1271,7 @@ const CONST = { IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', IMPORT_TRACK_CATEGORIES: 'importTrackingCategories', + MAPPINGS: 'mappings', TRACK_CATEGORY_PREFIX: 'trackingCategory_', TRACK_CATEGORY_FIELDS: { COST_CENTERS: 'cost centers', diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 4bb9af400440..56f5410c2ac5 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -15,6 +15,7 @@ import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import CONST from '@src/CONST'; import { TranslationPaths } from '@src/languages/types'; import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); @@ -55,12 +56,12 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { { + onSelectRow={(option) => { Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`]: row.value}: {}) + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) }) - Navigation.goBack(); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.getRoute(policyID)); }} /> diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 24d04be14804..c20aaaa24c07 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -7,28 +7,30 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import { TranslationPaths } from '@src/languages/types'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; + const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION); const optionsList = useMemo(() => { - const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION)?.value ?? ""; - return Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => { return { value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), keyForList: option, - isSelected: option.toLowerCase() === costCenterCategoryValue.toLowerCase() + isSelected: option.toLowerCase() === category?.value?.toLowerCase() } }); }, [policyID, translate]); @@ -36,7 +38,7 @@ function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { return ( {}} + onSelectRow={(option) => { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) + }) + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.getRoute(policyID)); + }} /> From 5c7ef82d85d789727cae211f93eb6f1b21031519 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Fri, 3 May 2024 11:15:29 +0530 Subject: [PATCH 090/219] style: lint fixes --- src/libs/actions/connections/ConnectToXero.ts | 2 +- .../XeroMapCostCentersToConfigurationPage.tsx | 20 ++++++++----------- .../XeroMapRegionsToConfigurationPage.tsx | 9 ++++----- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/connections/ConnectToXero.ts b/src/libs/actions/connections/ConnectToXero.ts index 11aa904a678a..51652c97bc49 100644 --- a/src/libs/actions/connections/ConnectToXero.ts +++ b/src/libs/actions/connections/ConnectToXero.ts @@ -15,7 +15,7 @@ const getTrackingCategory = (policy: OnyxEntry, key: string) = const { trackingCategories } = policy?.connections?.xero?.data ?? {}; const { mappings } = policy?.connections?.xero?.config ?? {}; - const category = trackingCategories?.find((category) => category.name.toLowerCase() === key.toLowerCase()); + const category = trackingCategories?.find((currentCategory) => currentCategory.name.toLowerCase() === key.toLowerCase()); if (!category) { return undefined; } diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 56f5410c2ac5..a70b5f2eb59a 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -24,18 +24,14 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); - const optionsList = useMemo(() => { - - - return Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => { - return { - value: option, - text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), - keyForList: option, - isSelected: option.toLowerCase() === category?.value?.toLowerCase() - } - }); - }, [policyID, translate]); + const optionsList = useMemo(() => ( + Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => ({ + value: option, + text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), + keyForList: option, + isSelected: option.toLowerCase() === category?.value?.toLowerCase() + })) + ), [policyID, translate, category]); return ( diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index c20aaaa24c07..21b702909485 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -24,16 +24,15 @@ function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { const policyID = policy?.id ?? ''; const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION); - const optionsList = useMemo(() => { - return Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => { - return { + const optionsList = useMemo(() => ( + Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => ( + { value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), keyForList: option, isSelected: option.toLowerCase() === category?.value?.toLowerCase() } - }); - }, [policyID, translate]); + ))), [policyID, translate, category]); return ( Date: Fri, 3 May 2024 11:46:55 +0530 Subject: [PATCH 091/219] style: lint fixes --- .../accounting/xero/XeroMapCostCentersToConfigurationPage.tsx | 4 ++-- .../accounting/xero/XeroMapRegionsToConfigurationPage.tsx | 4 ++-- .../accounting/xero/XeroTrackingCategoryConfigurationPage.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index a70b5f2eb59a..dd67501e0d7f 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -13,7 +13,7 @@ import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import CONST from '@src/CONST'; -import { TranslationPaths } from '@src/languages/types'; +import type { TranslationPaths } from '@src/languages/types'; import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; @@ -31,7 +31,7 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { keyForList: option, isSelected: option.toLowerCase() === category?.value?.toLowerCase() })) - ), [policyID, translate, category]); + ), [translate, category]); return ( diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 21b702909485..4e7f1c1933db 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -13,7 +13,7 @@ import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; -import { TranslationPaths } from '@src/languages/types'; +import type { TranslationPaths } from '@src/languages/types'; import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; @@ -32,7 +32,7 @@ function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { keyForList: option, isSelected: option.toLowerCase() === category?.value?.toLowerCase() } - ))), [policyID, translate, category]); + ))), [translate, category]); return ( Date: Fri, 3 May 2024 11:52:55 +0530 Subject: [PATCH 092/219] refactor: use connection layout --- .../XeroMapCostCentersToConfigurationPage.tsx | 23 +++++-------------- .../XeroMapRegionsToConfigurationPage.tsx | 23 +++++-------------- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index dd67501e0d7f..b61207fefb20 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -1,14 +1,10 @@ import React, { useMemo } from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; +import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; @@ -35,20 +31,14 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { return ( - - - - - {translate('workspace.xero.mapXeroCostCentersToDescription')} - - - + ); } diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 4e7f1c1933db..26026300cf46 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -1,14 +1,10 @@ import React, { useMemo } from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; +import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; @@ -35,20 +31,14 @@ function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { ))), [translate, category]); return ( - - - - - {translate('workspace.xero.mapXeroRegionsToDescription')} - - - + ); } From 01eda9e6cc64a66b3e7da996255c15072046859c Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Fri, 3 May 2024 12:00:34 +0530 Subject: [PATCH 093/219] refactor: use connection layout --- .../XeroTrackingCategoryConfigurationPage.tsx | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 23f0d0163522..eb392a9d225f 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -1,16 +1,13 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; import Switch from '@components/Switch'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; @@ -19,6 +16,7 @@ import ROUTES from '@src/ROUTES'; import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import type { TranslationPaths } from '@src/languages/types'; import type { MenuItemProps } from '@components/MenuItem'; +import ConnectionLayout from '@components/ConnectionLayout'; function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); @@ -54,20 +52,16 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { ); return ( - - - - - {translate('workspace.xero.trackingCategoriesDescription')} - + + {translate('workspace.accounting.import')} @@ -98,12 +92,12 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { description={menuItem.description} shouldShowRightIcon onPress={menuItem.onPress} + wrapperStyle={styles.sectionMenuItemTopDescription} /> ))} - )} - - + )} + ); } From 228b824431d4c4bc2a1510ea5a7c828462aec9db Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 3 May 2024 16:05:30 +0900 Subject: [PATCH 094/219] update spanish --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index cbedd0c555a4..d72645890008 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -603,7 +603,7 @@ export default { splitBill: 'Dividir gasto', splitScan: 'Dividir recibo', splitDistance: 'Dividir distancia', - sendMoney: 'Pagar a alguien', + paySomeone: ({name}: PaySomeoneParams) => `Pagar a ${name ?? 'alguien'}`, assignTask: 'Assignar tarea', header: 'Acción rápida', trackManual: 'Crear gasto', From c4f4af6083d925953d21577806c6f565675e9d4d Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 3 May 2024 16:13:10 +0900 Subject: [PATCH 095/219] Use better function --- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index c1ccf871d331..c982560d41a7 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -19,8 +19,10 @@ import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import Navigation from '@libs/Navigation/Navigation'; +import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy'; @@ -34,7 +36,6 @@ import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getDisplayNameOrDefault} from "@libs/PersonalDetailsUtils"; // On small screen we hide the search page from central pane to show the search bottom tab page with bottom tab bar. // We need to take this in consideration when checking if the screen is focused. @@ -198,12 +199,12 @@ function FloatingActionButtonAndPopover( return ''; } if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { - const name = getDisplayNameOrDefault(personalDetails?.[quickActionAvatars[0]?.id ?? 0]); - return translate('quickAction.paySomeone', {name}) + const name = getDisplayNameForParticipant(quickActionAvatars[0]?.id ?? 0, true) ?? ''; + return translate('quickAction.paySomeone', {name}); } const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); return titleKey ? translate(titleKey) : ''; - }, [quickAction, translate, quickActionAvatars, personalDetails, quickActionReport]); + }, [quickAction, translate, quickActionAvatars, quickActionReport]); const hideQABSubtitle = useMemo(() => { if (isEmptyObject(quickActionReport)) { From 7df397cca43a48cbfee3d9f392c429093dfea2a3 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 3 May 2024 16:22:45 +0900 Subject: [PATCH 096/219] lint --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index c982560d41a7..65ce074d5c50 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -19,7 +19,6 @@ import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import Navigation from '@libs/Navigation/Navigation'; -import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; @@ -199,7 +198,7 @@ function FloatingActionButtonAndPopover( return ''; } if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { - const name = getDisplayNameForParticipant(quickActionAvatars[0]?.id ?? 0, true) ?? ''; + const name: string = getDisplayNameForParticipant(quickActionAvatars[0]?.id ?? 0, true) ?? ''; return translate('quickAction.paySomeone', {name}); } const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); From 00f32136750087be861dd7eef9bfe7b1706769be Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 3 May 2024 16:54:07 +0900 Subject: [PATCH 097/219] typescript --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 65ce074d5c50..0e0c6fe1b084 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -198,7 +198,7 @@ function FloatingActionButtonAndPopover( return ''; } if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { - const name: string = getDisplayNameForParticipant(quickActionAvatars[0]?.id ?? 0, true) ?? ''; + const name: string = getDisplayNameForParticipant(+quickActionAvatars[0]?.id ?? 0, true) ?? ''; return translate('quickAction.paySomeone', {name}); } const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); From 47c539a45e978c08a54cd04bc16e8fcbb718ef94 Mon Sep 17 00:00:00 2001 From: Alberto Date: Fri, 3 May 2024 16:59:42 +0900 Subject: [PATCH 098/219] typescript --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 0e0c6fe1b084..2afb4b67dee9 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -198,7 +198,7 @@ function FloatingActionButtonAndPopover( return ''; } if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { - const name: string = getDisplayNameForParticipant(+quickActionAvatars[0]?.id ?? 0, true) ?? ''; + const name: string = getDisplayNameForParticipant(+(quickActionAvatars[0]?.id ?? 0), true) ?? ''; return translate('quickAction.paySomeone', {name}); } const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); From b012ed6968e4a00d3b97109e39d7959d8a8d5ba2 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Fri, 3 May 2024 20:43:21 +0530 Subject: [PATCH 099/219] refactor: remove unused importants --- .../accounting/xero/XeroMapCostCentersToConfigurationPage.tsx | 2 -- .../accounting/xero/XeroMapRegionsToConfigurationPage.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index b61207fefb20..6739940b8802 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -3,7 +3,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -15,7 +14,6 @@ import ROUTES from '@src/ROUTES'; function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const policyID = policy?.id ?? ''; const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 26026300cf46..53e12e57cb41 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -3,7 +3,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -16,7 +15,6 @@ import ROUTES from '@src/ROUTES'; function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const policyID = policy?.id ?? ''; const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION); From c556f5975de063b090d0902e1526bf698448c62a Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 3 May 2024 10:32:23 -0600 Subject: [PATCH 100/219] extend header functionality --- src/pages/Search/SearchPage.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 499b09fd4eb9..3c0d278bbd85 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {SearchQuery} from '@src/types/onyx/SearchResults'; +import IconAsset from '@src/types/utils/IconAsset'; type SearchPageProps = StackScreenProps; @@ -21,6 +22,10 @@ function SearchPage({route}: SearchPageProps) { const query = currentQuery as SearchQuery; const isValidQuery = Object.values(CONST.TAB_SEARCH).includes(query); + const headerContent: {[key in SearchQuery]: {icon: IconAsset, title: string}} = { + all: {icon: Illustrations.MoneyReceipts, title: translate('common.expenses')}, + } + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)); return ( @@ -32,8 +37,8 @@ function SearchPage({route}: SearchPageProps) { shouldShowLink={false} > From b17a2e34864add0350e14f3cf50a24535e67b2e0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 3 May 2024 11:29:07 -0600 Subject: [PATCH 101/219] extend getters --- src/CONST.ts | 4 ++++ src/components/Search.tsx | 6 ++++-- src/libs/SearchUtils.ts | 16 ++++++---------- src/types/onyx/SearchResults.ts | 20 +++++++++++++++++++- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index dd85fee354d4..7224bc1d367a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4716,6 +4716,10 @@ const CONST = { CARD: 'card', DISTANCE: 'distance', }, + + SEARCH_DATA_TYPES: { + TRANSACTION: 'transaction', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 3f248c0c72aa..69e2bc543366 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -15,6 +15,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import SelectionList from './SelectionList'; import SearchTableHeader from './SelectionList/SearchTableHeader'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; +import {SearchDataTypes} from '@src/types/onyx/SearchResults'; type SearchProps = { query: string; @@ -55,8 +56,9 @@ function Search({query}: SearchProps) { Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(query, reportID)); }; - const ListItem = SearchUtils.getListItem(); - const data = SearchUtils.getSections(searchResults?.data ?? {}); + const type = searchResults?.search?.type as SearchDataTypes; + const ListItem = SearchUtils.getListItem(type); + const data = SearchUtils.getSections(searchResults?.data ?? {}, type); const shouldShowMerchant = SearchUtils.getShouldShowMerchant(searchResults?.data ?? {}); return ( diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 570a8e780f5a..a4c5d13f57fd 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -2,7 +2,7 @@ import TransactionListItem from '@components/SelectionList/TransactionListItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {SearchTransaction} from '@src/types/onyx/SearchResults'; +import type {SearchTransaction, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; import * as UserUtils from './UserUtils'; function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { @@ -33,23 +33,19 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): SearchT }); } -const searchTypeToItemMap = { +const searchTypeToItemMap: SearchTypeToItemMap = { transaction: { listItem: TransactionListItem, getSections: getTransactionsSections, }, }; -/** - * TODO: in future make this function generic and return specific item component based on type - * For now only 1 search item type exists in the app so this function is simplified - */ -function getListItem(): typeof TransactionListItem { - return searchTypeToItemMap.transaction.listItem; +function getListItem(type: K): SearchTypeToItemMap[K]['listItem'] { + return searchTypeToItemMap[type].listItem; } -function getSections(data: OnyxTypes.SearchResults['data']): SearchTransaction[] { - return searchTypeToItemMap.transaction.getSections(data); +function getSections(data: OnyxTypes.SearchResults['data'], type: K): ReturnType { + return searchTypeToItemMap[type].getSections(data) as ReturnType; } function getQueryHash(query: string): number { diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index af23821d70e8..cb8618875702 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,6 +1,24 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type {Receipt} from './Transaction'; +import TransactionListItem from '@components/SelectionList/TransactionListItem'; + +type SearchDataTypes = ValueOf; + +type ListItemType = + T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? typeof TransactionListItem : + never; + +type SectionsType = + T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? SearchTransaction[] : + never; + +type SearchTypeToItemMap = { + [K in SearchDataTypes]: { + listItem: ListItemType; + getSections: (data: SearchResults['data']) => SectionsType; + } +}; type SearchResultsInfo = { offset: number; @@ -62,4 +80,4 @@ type SearchResults = { export default SearchResults; -export type {SearchQuery, SearchTransaction, SearchTransactionType, SearchPersonalDetails, SearchPolicyDetails}; +export type {SearchQuery, SearchTransaction, SearchTransactionType, SearchPersonalDetails, SearchPolicyDetails, SearchDataTypes, SearchTypeToItemMap}; From d3730090e02bc025cd7b233565d9841a7f04fd05 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Sat, 4 May 2024 01:24:42 +0700 Subject: [PATCH 102/219] fix eslint --- src/components/DraggableList/index.android.tsx | 8 ++------ src/components/MapView/responder/index.android.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/DraggableList/index.android.tsx b/src/components/DraggableList/index.android.tsx index 7d1add9d3da1..61527544abea 100644 --- a/src/components/DraggableList/index.android.tsx +++ b/src/components/DraggableList/index.android.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import {View} from 'react-native'; import DraggableFlatList from 'react-native-draggable-flatlist'; import type {FlatList} from 'react-native-gesture-handler'; import useThemeStyles from '@hooks/useThemeStyles'; -import { View } from 'react-native'; import type {DraggableListProps} from './types'; function DraggableList({renderClone, shouldUsePortal, ListFooterComponent, ...viewProps}: DraggableListProps, ref: React.ForwardedRef>) { @@ -16,11 +16,7 @@ function DraggableList({renderClone, shouldUsePortal, ListFooterComponent, .. // eslint-disable-next-line react/jsx-props-no-spreading {...viewProps} /> - {ListFooterComponent && ( - - {ListFooterComponent} - - )} + {ListFooterComponent && {ListFooterComponent}}
); } diff --git a/src/components/MapView/responder/index.android.ts b/src/components/MapView/responder/index.android.ts index e1907e786733..9cbcde11ca7b 100644 --- a/src/components/MapView/responder/index.android.ts +++ b/src/components/MapView/responder/index.android.ts @@ -1,11 +1,11 @@ -import { PanResponder } from 'react-native'; +import {PanResponder} from 'react-native'; const InterceptPanResponderCapture = PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onStartShouldSetPanResponderCapture: () => true, - onMoveShouldSetPanResponder: () => true, - onMoveShouldSetPanResponderCapture: () => true, - onPanResponderTerminationRequest: () => false, + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderTerminationRequest: () => false, }); export default InterceptPanResponderCapture; From cb62e4a620a1d70a3492951959c1a3f9fc2f65a1 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 3 May 2024 15:33:27 -0600 Subject: [PATCH 103/219] fix lint --- src/components/Search.tsx | 2 +- src/pages/Search/SearchPage.tsx | 4 ++-- src/types/onyx/SearchResults.ts | 12 ++++-------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 69e2bc543366..18fc810577eb 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -10,12 +10,12 @@ import EmptySearchView from '@pages/Search/EmptySearchView'; import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {SearchDataTypes} from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import SelectionList from './SelectionList'; import SearchTableHeader from './SelectionList/SearchTableHeader'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; -import {SearchDataTypes} from '@src/types/onyx/SearchResults'; type SearchProps = { query: string; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 3c0d278bbd85..cd744287664c 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -22,9 +22,9 @@ function SearchPage({route}: SearchPageProps) { const query = currentQuery as SearchQuery; const isValidQuery = Object.values(CONST.TAB_SEARCH).includes(query); - const headerContent: {[key in SearchQuery]: {icon: IconAsset, title: string}} = { + const headerContent: {[key in SearchQuery]: {icon: IconAsset; title: string}} = { all: {icon: Illustrations.MoneyReceipts, title: translate('common.expenses')}, - } + }; const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)); diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index cb8618875702..589bb876b933 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,23 +1,19 @@ import type {ValueOf} from 'type-fest'; +import TransactionListItem from '@components/SelectionList/TransactionListItem'; import type CONST from '@src/CONST'; import type {Receipt} from './Transaction'; -import TransactionListItem from '@components/SelectionList/TransactionListItem'; type SearchDataTypes = ValueOf; -type ListItemType = - T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? typeof TransactionListItem : - never; +type ListItemType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? typeof TransactionListItem : never; -type SectionsType = - T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? SearchTransaction[] : - never; +type SectionsType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? SearchTransaction[] : never; type SearchTypeToItemMap = { [K in SearchDataTypes]: { listItem: ListItemType; getSections: (data: SearchResults['data']) => SectionsType; - } + }; }; type SearchResultsInfo = { From f69525ffdbed44f371909ded3eb3f7cc3a9e1f71 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Sat, 4 May 2024 15:34:42 +0700 Subject: [PATCH 104/219] update condition check isValidElement --- src/components/DraggableList/index.android.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DraggableList/index.android.tsx b/src/components/DraggableList/index.android.tsx index 61527544abea..30bf7c927bd9 100644 --- a/src/components/DraggableList/index.android.tsx +++ b/src/components/DraggableList/index.android.tsx @@ -16,7 +16,7 @@ function DraggableList({renderClone, shouldUsePortal, ListFooterComponent, .. // eslint-disable-next-line react/jsx-props-no-spreading {...viewProps} /> - {ListFooterComponent && {ListFooterComponent}} + {React.isValidElement(ListFooterComponent) && {ListFooterComponent}}
); } From c68c7aaddc529327c1cf4db4ad77c48080883381 Mon Sep 17 00:00:00 2001 From: Anusha Date: Sat, 4 May 2024 16:23:45 +0500 Subject: [PATCH 105/219] Fix disabled Tabs --- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 1c861d510a85..ea5e40872351 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -84,7 +84,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const styles = useThemeStyles(); const policy = policyDraft?.id ? policyDraft : policyProp; const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); - const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors); + const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors)); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); const activeRoute = useNavigationState(getTopmostWorkspacesCentralPaneName); From aed9941214642f3080f04712931efd06389dba3e Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Sun, 5 May 2024 00:01:01 +0530 Subject: [PATCH 106/219] fix: update routes --- src/ROUTES.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f9c47857ba1d..0a4459c23acb 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -781,16 +781,16 @@ const ROUTES = { getRoute: (policyID: string, currentOrganizationID: string) => `settings/workspaces/${policyID}/accounting/xero/organization/${currentOrganizationID}` as const, }, POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES: { - route: 'settings/workspaces/:policyID/accounting/xero/import/trackCategories', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/trackCategories` as const, + route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories` as const, }, POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS: { - route: 'settings/workspaces/:policyID/accounting/xero/import/trackCategories/mapCostCenters', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/trackCategories/mapCostCenters` as const, + route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/cost-centers', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/cost-centers` as const, }, POLICY_ACCOUNTING_XERO_MAP_REGIONS: { - route: 'settings/workspaces/:policyID/accounting/xero/import/trackCategories/mapRegions', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/trackCategories/mapRegions` as const, + route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/region', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categoriess/region` as const, }, POLICY_ACCOUNTING_XERO_CUSTOMER: { route: '/settings/workspaces/:policyID/accounting/xero/import/customers', From 1b58ea4aadf3adac238f35c1e621c098cee25c13 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Sun, 5 May 2024 00:19:00 +0530 Subject: [PATCH 107/219] refactor: rename keys --- src/CONST.ts | 8 ++++---- src/ROUTES.ts | 6 +++--- src/SCREENS.ts | 4 ++-- .../AppNavigator/ModalStackNavigators/index.tsx | 4 ++-- .../linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts | 4 ++-- src/libs/Navigation/linkingConfig/config.ts | 6 +++--- src/libs/Navigation/types.ts | 9 +++++++-- src/libs/actions/connections/ConnectToXero.ts | 2 +- .../workspace/accounting/xero/XeroImportPage.tsx | 2 +- .../xero/XeroMapCostCentersToConfigurationPage.tsx | 8 ++++---- .../xero/XeroMapRegionsToConfigurationPage.tsx | 8 ++++---- .../xero/XeroTrackingCategoryConfigurationPage.tsx | 12 ++++++------ 12 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 5b68e5ae21c5..9fb61cee5adb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1283,14 +1283,14 @@ const CONST = { SYNC: 'sync', IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', - IMPORT_TRACK_CATEGORIES: 'importTrackingCategories', + IMPORT_TRACKING_CATEGORIES: 'importTrackingCategories', MAPPINGS: 'mappings', - TRACK_CATEGORY_PREFIX: 'trackingCategory_', - TRACK_CATEGORY_FIELDS: { + TRACKING_CATEGORY_PREFIX: 'trackingCategory_', + TRACKING_CATEGORY_FIELDS: { COST_CENTERS: 'cost centers', REGION: 'region', }, - TRACK_CATEGORY_OPTIONS: { + TRACKING_CATEGORY_OPTIONS: { DEFAULT: 'DEFAULT', TAG: 'TAG' } diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0a4459c23acb..f1748a3d5506 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -780,15 +780,15 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/organization/:currentOrganizationID', getRoute: (policyID: string, currentOrganizationID: string) => `settings/workspaces/${policyID}/accounting/xero/organization/${currentOrganizationID}` as const, }, - POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES: { + POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES: { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories` as const, }, - POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS: { + POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS: { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/cost-centers', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/cost-centers` as const, }, - POLICY_ACCOUNTING_XERO_MAP_REGIONS: { + POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION: { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/region', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categoriess/region` as const, }, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9fd4d5582350..1cb0d7e4016b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -244,9 +244,9 @@ const SCREENS = { XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers', XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', - XERO_TRACK_CATEGORIES: 'Policy_Accounting_Xero_Track_Categories', + XERO_TRACKING_CATEGORIES: 'Policy_Accounting_Xero_Tracking_Categories', XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', - XERO_MAP_REGIONS: 'Policy_Accounting_Xero_Map_Regions', + XERO_MAP_REGION: 'Policy_Accounting_Xero_Map_Region', XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', }, INITIAL: 'Workspace_Initial', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 77d7aa4f75e0..c4785dca7187 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -305,9 +305,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/XeroOrganizationConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: () => require('../../../../pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: () => require('../../../../pages/workspace/accounting/xero/XeroTaxesConfigurationPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: () => require('../../../../pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACKING_CATEGORIES]: () => require('../../../../pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGION]: () => require('../../../../pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: () => require('../../../../pages/workspace/accounting/xero/advanced/XeroAdvancedPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 43445fb234e5..503909121c00 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -44,9 +44,9 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION, SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER, SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES, - SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES, + SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACKING_CATEGORIES, SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS, - SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS, + SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGION, SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED, ], [SCREENS.WORKSPACE.TAXES]: [ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 4147369084aa..2883195f1d94 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -329,9 +329,9 @@ const config: LinkingOptions['config'] = { }, [SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.route}, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.route}, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: {path: ROUTES.POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS.route}, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGIONS]: {path: ROUTES.POLICY_ACCOUNTING_XERO_MAP_REGIONS.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACKING_CATEGORIES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS.route}, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: {path: ROUTES.POLICY_ACCOUNTING_XERO_CUSTOMER.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TAXES.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.route}, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e1f61d08a824..fde07e275aa6 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -324,8 +324,13 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: { policyID: string; }; - - [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACK_CATEGORIES]: { + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACKING_CATEGORIES]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGION]: { policyID: string; }; [SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: { diff --git a/src/libs/actions/connections/ConnectToXero.ts b/src/libs/actions/connections/ConnectToXero.ts index 51652c97bc49..7205690d2619 100644 --- a/src/libs/actions/connections/ConnectToXero.ts +++ b/src/libs/actions/connections/ConnectToXero.ts @@ -22,7 +22,7 @@ const getTrackingCategory = (policy: OnyxEntry, key: string) = return { ...category, - value: mappings?.[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`] ?? "" + value: mappings?.[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`] ?? "" }; } diff --git a/src/pages/workspace/accounting/xero/XeroImportPage.tsx b/src/pages/workspace/accounting/xero/XeroImportPage.tsx index 4a0db25e9947..fbd0d6add7e5 100644 --- a/src/pages/workspace/accounting/xero/XeroImportPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroImportPage.tsx @@ -36,7 +36,7 @@ function XeroImportPage({policy}: WithPolicyProps) { }, { description: translate('workspace.xero.trackingCategories'), - action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.getRoute(policyID)), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)), hasError: !!policy?.errors?.importTrackingCategories, title: importTrackingCategories ? translate('workspace.accounting.importTypes.TAG') : translate('workspace.xero.notImported'), pendingAction: pendingFields?.importTrackingCategories, diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 6739940b8802..13282da8a5e3 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -16,10 +16,10 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? ''; - const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS); + const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS); const optionsList = useMemo(() => ( - Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => ({ + Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).map((option) => ({ value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), keyForList: option, @@ -43,9 +43,9 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { onSelectRow={(option) => { Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) }) - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.getRoute(policyID)); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); }} /> diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 53e12e57cb41..d150ebae3589 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -16,10 +16,10 @@ import ROUTES from '@src/ROUTES'; function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? ''; - const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION); + const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION); const optionsList = useMemo(() => ( - Object.values(CONST.XERO_CONFIG.TRACK_CATEGORY_OPTIONS).map((option) => ( + Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).map((option) => ( { value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), @@ -43,9 +43,9 @@ function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { onSelectRow={(option) => { Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACK_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) }) - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACK_CATEGORIES.getRoute(policyID)); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); }} /> diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index eb392a9d225f..20c74dce1cf9 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -29,20 +29,20 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { () => { const availableCategories = []; - const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.COST_CENTERS)?.value ?? ""; - const regionCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION)?.value ?? ""; + const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS)?.value ?? ""; + const regionCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION)?.value ?? ""; if (costCenterCategoryValue) { availableCategories.push({ description: translate('workspace.xero.mapXeroCostCentersTo'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_COST_CENTERS.getRoute(policyID)), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS.getRoute(policyID)), title: translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths) }); } - if (trackingCategories?.find((category) => category.name.toLowerCase() === CONST.XERO_CONFIG.TRACK_CATEGORY_FIELDS.REGION)) { + if (trackingCategories?.find((category) => category.name.toLowerCase() === CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION)) { availableCategories.push({ description: translate('workspace.xero.mapXeroRegionsTo'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_MAP_REGIONS.getRoute(policyID)), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION.getRoute(policyID)), title: translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths) }); } @@ -74,7 +74,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { Connections.updatePolicyConnectionConfig( policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, - CONST.XERO_CONFIG.IMPORT_TRACK_CATEGORIES, + CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES, !importTrackingCategories, ) } From d01ecf78b468edf0f13cf529dc3792c05130485c Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Sun, 5 May 2024 00:23:01 +0530 Subject: [PATCH 108/219] style: lint fixes --- src/CONST.ts | 4 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- .../ModalStackNavigators/index.tsx | 3 +- src/libs/actions/connections/ConnectToXero.ts | 10 +- .../XeroMapCostCentersToConfigurationPage.tsx | 51 +++++----- .../XeroMapRegionsToConfigurationPage.tsx | 48 +++++----- .../XeroTrackingCategoryConfigurationPage.tsx | 95 +++++++++---------- src/types/onyx/Policy.ts | 2 +- 9 files changed, 108 insertions(+), 109 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 9fb61cee5adb..23b16d217bb4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1292,8 +1292,8 @@ const CONST = { }, TRACKING_CATEGORY_OPTIONS: { DEFAULT: 'DEFAULT', - TAG: 'TAG' - } + TAG: 'TAG', + }, }, QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE: { diff --git a/src/languages/en.ts b/src/languages/en.ts index b46f2126d706..57429a971484 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2031,7 +2031,7 @@ export default { notImported: 'Not imported', trackingCategoriesOptions: { default: 'Xero contact default', - tag: 'Tags' + tag: 'Tags', }, advancedConfig: { advanced: 'Advanced', diff --git a/src/languages/es.ts b/src/languages/es.ts index 339fda0b36c5..db4f1231a82a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2064,7 +2064,7 @@ export default { notImported: 'No importado', trackingCategoriesOptions: { default: 'Contacto de Xero por defecto', - tag: 'Etiquetas' + tag: 'Etiquetas', }, advancedConfig: { advanced: 'Avanzado', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c4785dca7187..86642efc8545 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -305,7 +305,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/XeroOrganizationConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: () => require('../../../../pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: () => require('../../../../pages/workspace/accounting/xero/XeroTaxesConfigurationPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACKING_CATEGORIES]: () => require('../../../../pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.XERO_TRACKING_CATEGORIES]: () => + require('../../../../pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_COST_CENTERS]: () => require('../../../../pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_MAP_REGION]: () => require('../../../../pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: () => require('../../../../pages/workspace/accounting/xero/advanced/XeroAdvancedPage').default as React.ComponentType, diff --git a/src/libs/actions/connections/ConnectToXero.ts b/src/libs/actions/connections/ConnectToXero.ts index 7205690d2619..fde22855a618 100644 --- a/src/libs/actions/connections/ConnectToXero.ts +++ b/src/libs/actions/connections/ConnectToXero.ts @@ -1,8 +1,8 @@ +import type {OnyxEntry} from 'react-native-onyx'; import type {ConnectPolicyToAccountingIntegrationParams} from '@libs/API/parameters'; import {READ_COMMANDS} from '@libs/API/types'; import {getCommandURL} from '@libs/ApiUtils'; import CONST from '@src/CONST'; -import type {OnyxEntry} from 'react-native-onyx'; import type * as OnyxTypes from '@src/types/onyx'; const getXeroSetupLink = (policyID: string) => { @@ -12,8 +12,8 @@ const getXeroSetupLink = (policyID: string) => { }; const getTrackingCategory = (policy: OnyxEntry, key: string) => { - const { trackingCategories } = policy?.connections?.xero?.data ?? {}; - const { mappings } = policy?.connections?.xero?.config ?? {}; + const {trackingCategories} = policy?.connections?.xero?.data ?? {}; + const {mappings} = policy?.connections?.xero?.config ?? {}; const category = trackingCategories?.find((currentCategory) => currentCategory.name.toLowerCase() === key.toLowerCase()); if (!category) { @@ -22,8 +22,8 @@ const getTrackingCategory = (policy: OnyxEntry, key: string) = return { ...category, - value: mappings?.[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`] ?? "" + value: mappings?.[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`] ?? '', }; -} +}; export {getXeroSetupLink, getTrackingCategory}; diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 13282da8a5e3..a2c1b6421abb 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -1,32 +1,33 @@ -import React, { useMemo } from 'react'; +import React, {useMemo} from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import * as Connections from '@libs/actions/connections'; +import {getTrackingCategory} from '@libs/actions/connections/ConnectToXero'; +import Navigation from '@libs/Navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; -import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; import CONST from '@src/CONST'; -import type { TranslationPaths } from '@src/languages/types'; -import Navigation from '@libs/Navigation/Navigation'; +import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? ''; - const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS); - - const optionsList = useMemo(() => ( - Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).map((option) => ({ - value: option, - text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), - keyForList: option, - isSelected: option.toLowerCase() === category?.value?.toLowerCase() - })) - ), [translate, category]); + const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS); + const optionsList = useMemo( + () => + Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).map((option) => ({ + value: option, + text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), + keyForList: option, + isSelected: option.toLowerCase() === category?.value?.toLowerCase(), + })), + [translate, category], + ); return ( - { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { - ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) - }) - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); - }} - /> + { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), + }); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); + }} + /> ); } diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index d150ebae3589..043215339229 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -1,32 +1,32 @@ -import React, { useMemo } from 'react'; +import React, {useMemo} from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import * as Connections from '@libs/actions/connections'; +import {getTrackingCategory} from '@libs/actions/connections/ConnectToXero'; +import Navigation from '@libs/Navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; -import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; -import type { TranslationPaths } from '@src/languages/types'; -import Navigation from '@libs/Navigation/Navigation'; +import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; - function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? ''; - const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION); + const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION); - const optionsList = useMemo(() => ( - Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).map((option) => ( - { + const optionsList = useMemo( + () => + Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).map((option) => ({ value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), keyForList: option, - isSelected: option.toLowerCase() === category?.value?.toLowerCase() - } - ))), [translate, category]); + isSelected: option.toLowerCase() === category?.value?.toLowerCase(), + })), + [translate, category], + ); return ( - { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { - ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value}: {}) - }) - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); - }} - /> - + { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), + }); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); + }} + /> + ); } diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 20c74dce1cf9..53f03fbaa485 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -1,5 +1,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import ConnectionLayout from '@components/ConnectionLayout'; +import type {MenuItemProps} from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Switch from '@components/Switch'; @@ -7,35 +9,32 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; +import {getTrackingCategory} from '@libs/actions/connections/ConnectToXero'; import Navigation from '@libs/Navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; -import { getTrackingCategory } from '@libs/actions/connections/ConnectToXero'; -import type { TranslationPaths } from '@src/languages/types'; -import type { MenuItemProps } from '@components/MenuItem'; -import ConnectionLayout from '@components/ConnectionLayout'; function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; const {importTrackingCategories, pendingFields} = policy?.connections?.xero?.config ?? {}; - const { trackingCategories } = policy?.connections?.xero?.data ?? {}; + const {trackingCategories} = policy?.connections?.xero?.data ?? {}; - const menuItems: MenuItemProps[] = useMemo( - () => { + const menuItems: MenuItemProps[] = useMemo(() => { const availableCategories = []; - const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS)?.value ?? ""; - const regionCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION)?.value ?? ""; + const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS)?.value ?? ''; + const regionCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION)?.value ?? ''; if (costCenterCategoryValue) { availableCategories.push({ description: translate('workspace.xero.mapXeroCostCentersTo'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS.getRoute(policyID)), - title: translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths) + title: translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths), }); } @@ -43,13 +42,11 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { availableCategories.push({ description: translate('workspace.xero.mapXeroRegionsTo'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION.getRoute(policyID)), - title: translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths) + title: translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths), }); } return availableCategories; - }, - [translate, policy, policyID, trackingCategories], - ); + }, [translate, policy, policyID, trackingCategories]); return ( - - - {translate('workspace.accounting.import')} - - - - - Connections.updatePolicyConnectionConfig( - policyID, - CONST.POLICY.CONNECTIONS.NAME.XERO, - CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES, - !importTrackingCategories, - ) - } - /> - - + + + {translate('workspace.accounting.import')} - - {importTrackingCategories && ( - - {menuItems.map((menuItem: MenuItemProps) => ( - + + + Connections.updatePolicyConnectionConfig( + policyID, + CONST.POLICY.CONNECTIONS.NAME.XERO, + CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES, + !importTrackingCategories, + ) + } /> - ))} - - )} - + + + + + {importTrackingCategories && ( + + {menuItems.map((menuItem: MenuItemProps) => ( + + ))} + + )} + ); } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 7db56c5a96f8..002e8820504a 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -208,7 +208,7 @@ type Tenant = { type XeroTrackingCategory = { id: string; name: string; -} +}; type XeroConnectionData = { bankAccounts: unknown[]; From ffd6e3c6cc95ddb8d68caf0233514af1762f374d Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Sun, 5 May 2024 00:25:30 +0530 Subject: [PATCH 109/219] fix: typo in url --- src/ROUTES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f1748a3d5506..52eeef6c424f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -790,7 +790,7 @@ const ROUTES = { }, POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION: { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/region', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categoriess/region` as const, + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/region` as const, }, POLICY_ACCOUNTING_XERO_CUSTOMER: { route: '/settings/workspaces/:policyID/accounting/xero/import/customers', From 2fefa854e9d175eb1cf5281299ff97a6ca982060 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Sun, 5 May 2024 11:58:11 +0530 Subject: [PATCH 110/219] refactor: move api call to useCallback --- .../XeroMapCostCentersToConfigurationPage.tsx | 21 ++++++++++++------- .../XeroMapRegionsToConfigurationPage.tsx | 20 +++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index a2c1b6421abb..4635c712580e 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React, {useMemo, useCallback} from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -29,6 +29,17 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { [translate, category], ); + const updateMapping = useCallback((option: {value: string}) => { + if (option.value !== category?.value) { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), + }); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); + }, [category, policyID,policy?.connections?.xero?.config?.mappings]); + + return ( { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { - ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), - }); - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); - }} + onSelectRow={updateMapping} /> ); diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 043215339229..cc5abd1678df 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -28,6 +28,16 @@ function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { [translate, category], ); + const updateMapping = useCallback((option: {value: string}) => { + if (option.value !== category?.value) { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), + }); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); + }, [category, policyID,policy?.connections?.xero?.config?.mappings]); + return ( { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { - ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), - }); - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); - }} + onSelectRow={updateMapping} /> ); From cf0adbe001e51db229865954f3eb4945af052b46 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Sun, 5 May 2024 18:42:43 +0530 Subject: [PATCH 111/219] refactor: prettier fix --- .../XeroMapCostCentersToConfigurationPage.tsx | 24 ++++++++++--------- .../XeroMapRegionsToConfigurationPage.tsx | 21 +++++++++------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 4635c712580e..0d7e74d3b3e9 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -29,16 +29,18 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { [translate, category], ); - const updateMapping = useCallback((option: {value: string}) => { - if (option.value !== category?.value) { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { - ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), - }); - } - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); - }, [category, policyID,policy?.connections?.xero?.config?.mappings]); - + const updateMapping = useCallback( + (option: {value: string}) => { + if (option.value !== category?.value) { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), + }); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); + }, + [category, policyID, policy?.connections?.xero?.config?.mappings], + ); return ( { - if (option.value !== category?.value) { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { - ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), - }); - } - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); - }, [category, policyID,policy?.connections?.xero?.config?.mappings]); + const updateMapping = useCallback( + (option: {value: string}) => { + if (option.value !== category?.value) { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { + ...(policy?.connections?.xero?.config?.mappings ?? {}), + ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), + }); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); + }, + [category, policyID, policy?.connections?.xero?.config?.mappings], + ); return ( Date: Sun, 5 May 2024 18:49:25 +0530 Subject: [PATCH 112/219] docs: added comments --- src/libs/actions/connections/ConnectToXero.ts | 12 ++++++++++-- src/types/onyx/Policy.ts | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/connections/ConnectToXero.ts b/src/libs/actions/connections/ConnectToXero.ts index fde22855a618..43972e540d58 100644 --- a/src/libs/actions/connections/ConnectToXero.ts +++ b/src/libs/actions/connections/ConnectToXero.ts @@ -4,6 +4,7 @@ import {READ_COMMANDS} from '@libs/API/types'; import {getCommandURL} from '@libs/ApiUtils'; import CONST from '@src/CONST'; import type * as OnyxTypes from '@src/types/onyx'; +import type {XeroTrackingCategory} from '@src/types/onyx/Policy'; const getXeroSetupLink = (policyID: string) => { const params: ConnectPolicyToAccountingIntegrationParams = {policyID}; @@ -11,11 +12,18 @@ const getXeroSetupLink = (policyID: string) => { return commandURL + new URLSearchParams(params).toString(); }; -const getTrackingCategory = (policy: OnyxEntry, key: string) => { +/** + * Fetches the category object from the xero.data.trackingCategories based on the category name. + * This is required to get Xero category object with current value stored in the xero.config.mappings + * @param policy + * @param key + * @returns Filtered category matching the category name or undefined. + */ +const getTrackingCategory = (policy: OnyxEntry, categoryName: string): (XeroTrackingCategory & {value: string}) | undefined => { const {trackingCategories} = policy?.connections?.xero?.data ?? {}; const {mappings} = policy?.connections?.xero?.config ?? {}; - const category = trackingCategories?.find((currentCategory) => currentCategory.name.toLowerCase() === key.toLowerCase()); + const category = trackingCategories?.find((currentCategory) => currentCategory.name.toLowerCase() === categoryName.toLowerCase()); if (!category) { return undefined; } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 002e8820504a..e23935ba6857 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -570,4 +570,5 @@ export type { QBONonReimbursableExportAccountType, QBOReimbursableExportAccountType, QBOConnectionConfig, + XeroTrackingCategory, }; From dd38d835f79d4551646698f6c5a0f503b5b1a99c Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Mon, 6 May 2024 09:39:14 +0530 Subject: [PATCH 113/219] refactor: connection layout alignment --- .../accounting/xero/XeroMapCostCentersToConfigurationPage.tsx | 4 ++++ .../accounting/xero/XeroMapRegionsToConfigurationPage.tsx | 4 ++++ .../accounting/xero/XeroTrackingCategoryConfigurationPage.tsx | 1 + 3 files changed, 9 insertions(+) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 0d7e74d3b3e9..4280bcedbf3e 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -3,6 +3,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import {getTrackingCategory} from '@libs/actions/connections/ConnectToXero'; import Navigation from '@libs/Navigation/Navigation'; @@ -14,6 +15,8 @@ import ROUTES from '@src/ROUTES'; function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? ''; const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS); @@ -50,6 +53,7 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID && category?.id ? policyID : ''} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + titleStyle={[styles.pb2, styles.ph5]} > From 4c12752804ee1f5688656ce0a9b069ed7f62da0a Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 6 May 2024 11:09:58 +0700 Subject: [PATCH 114/219] update translation --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index b379a342de1d..fb97e1ce675f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -710,7 +710,7 @@ export default { other: 'Error inesperado, por favor inténtalo más tarde.', genericCreateFailureMessage: 'Error inesperado al enviar este gasto. Por favor, inténtalo más tarde.', genericCreateInvoiceFailureMessage: 'Error inesperado al enviar la factura, inténtalo de nuevo más tarde.', - receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Vuelva a intentarlo más tarde.', + receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Vuelve a intentarlo más tarde.', // eslint-disable-next-line rulesdir/use-periods-for-error-messages receiptFailureMessage: 'El recibo no se subió. ', // eslint-disable-next-line rulesdir/use-periods-for-error-messages From 016944fd82f9e18812448e7393f95c9dadcfa2ad Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 6 May 2024 11:13:18 +0700 Subject: [PATCH 115/219] Fix close button is not aligned --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 9b7bfb2c2f17..c3c505cfce2c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -346,7 +346,7 @@ function MoneyRequestView({ { if (!transaction?.transactionID) { return; From 2df2388052f72fe7c00bde36b1c486067466b6f0 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Mon, 6 May 2024 11:28:12 +0530 Subject: [PATCH 116/219] style: lint fix --- .../accounting/xero/XeroMapRegionsToConfigurationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx index 17d8f5a9575a..b02a8aa7fd5d 100644 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx @@ -16,7 +16,7 @@ import ROUTES from '@src/ROUTES'; function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - + const policyID = policy?.id ?? ''; const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION); From 67630b47d7a80e9ac4b3cddf6bedea254fe0ceff Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Mon, 6 May 2024 12:05:51 +0530 Subject: [PATCH 117/219] refactor: remove unnecessary code --- .../xero/XeroTrackingCategoryConfigurationPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 781fae4ff8a6..246e10611bcb 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -23,7 +23,6 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id ?? ''; const {importTrackingCategories, pendingFields} = policy?.connections?.xero?.config ?? {}; - const {trackingCategories} = policy?.connections?.xero?.data ?? {}; const menuItems: MenuItemProps[] = useMemo(() => { const availableCategories = []; @@ -38,7 +37,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { }); } - if (trackingCategories?.find((category) => category.name.toLowerCase() === CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION)) { + if (regionCategoryValue) { availableCategories.push({ description: translate('workspace.xero.mapXeroRegionsTo'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION.getRoute(policyID)), @@ -46,7 +45,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { }); } return availableCategories; - }, [translate, policy, policyID, trackingCategories]); + }, [translate, policy, policyID]); return ( Date: Mon, 6 May 2024 16:17:15 +0700 Subject: [PATCH 118/219] rename event --- src/libs/actions/Report.ts | 2 +- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d413b15f67aa..a20dde56b4aa 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1253,7 +1253,7 @@ function handleReportChanged(report: OnyxEntry) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? ''), CONST.NAVIGATION.TYPE.FORCED_UP); }; } - DeviceEventEmitter.emit(`switchToCurrentReport_${report.reportID}`, { + DeviceEventEmitter.emit(`switchToPreExistingReport_${report.reportID}`, { preexistingReportID: report.preexistingReportID, callback, }); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 5ae49c322749..3120bbe9bed2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -345,7 +345,7 @@ function ComposerWithSuggestions( ); useEffect(() => { - const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToCurrentReport_${reportID}`, ({preexistingReportID, callback}) => { + const switchToCurrentReport = DeviceEventEmitter.addListener(`switchToPreExistingReport_${reportID}`, ({preexistingReportID, callback}) => { if (!commentRef.current) { callback(); return; From 0f1e6981e226dcf7a060f1a71df6c30f8aeb9962 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 6 May 2024 13:57:31 +0200 Subject: [PATCH 119/219] address comments --- .../MoneyRequestParticipantsSelector.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 58db91c0f240..24221a700b35 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -75,7 +75,7 @@ function MoneyRequestParticipantsSelector({ const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; - const isCategorizeOrShareAction = ([CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE] as string[]).includes(action); + const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action); const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE; @@ -105,14 +105,14 @@ function MoneyRequestParticipantsSelector({ // sees the option to submit an expense from their admin on their own Workspace Chat. (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.SPLIT) && action !== CONST.IOU.ACTION.SUBMIT, - (canUseP2PDistanceRequests ?? iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, + (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, {}, [], false, {}, [], - (canUseP2PDistanceRequests ?? iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, + (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, undefined, undefined, @@ -124,7 +124,14 @@ function MoneyRequestParticipantsSelector({ isCategorizeOrShareAction ? 0 : undefined, ); - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(debouncedSearchTerm, participants.map((participant) => ({...participant, reportID: participant.reportID ?? ''})), chatOptions.recentReports, chatOptions.personalDetails, personalDetails, true); + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( + debouncedSearchTerm, + participants.map((participant) => ({...participant, reportID: participant.reportID ?? ''})), + chatOptions.recentReports, + chatOptions.personalDetails, + personalDetails, + true, + ); newSections.push(formatResults.section); @@ -268,8 +275,8 @@ function MoneyRequestParticipantsSelector({ // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet const isAllowedToSplit = (canUseP2PDistanceRequests ?? iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && - !([CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE] as string[]).includes(iouType) && - !([CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE] as string[]).includes(action); + ![CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE].some((option) => option === iouType) && + ![CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE].some((option) => option === action); const handleConfirmSelection = useCallback( (keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Participant) => { From 43cb0109ba834ebc53fb98e1374cff21be0e4486 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 6 May 2024 14:17:50 +0200 Subject: [PATCH 120/219] fix lint --- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 24221a700b35..e2965aba73ea 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -105,6 +105,7 @@ function MoneyRequestParticipantsSelector({ // sees the option to submit an expense from their admin on their own Workspace Chat. (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.SPLIT) && action !== CONST.IOU.ACTION.SUBMIT, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, {}, @@ -112,6 +113,7 @@ function MoneyRequestParticipantsSelector({ false, {}, [], + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction, false, undefined, From 1ad61dcdc2b5d292e27ecb6663dd8bf4506e83fb Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Mon, 6 May 2024 16:07:27 +0200 Subject: [PATCH 121/219] fix: restore picker done button --- .../reimburse/WorkspaceRateAndUnitPage.tsx | 158 ------------------ src/styles/index.ts | 5 + 2 files changed, 5 insertions(+), 158 deletions(-) delete mode 100644 src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx deleted file mode 100644 index 8fef5f4dc6f9..000000000000 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect} from 'react'; -import {Keyboard, View} from 'react-native'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormOnyxValues} from '@components/Form/types'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import Picker from '@components/Picker'; -import TextInput from '@components/TextInput'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import getPermittedDecimalSeparator from '@libs/getPermittedDecimalSeparator'; -import Navigation from '@libs/Navigation/Navigation'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import * as NumberUtils from '@libs/NumberUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import withPolicy from '@pages/workspace/withPolicy'; -import type {WithPolicyProps} from '@pages/workspace/withPolicy'; -import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import * as BankAccounts from '@userActions/BankAccounts'; -import * as Policy from '@userActions/Policy'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import type {Unit} from '@src/types/onyx/Policy'; - -type WorkspaceRateAndUnitPageProps = WithPolicyProps & StackScreenProps; - -type ValidationError = {rate?: TranslationPaths | undefined}; - -function WorkspaceRateAndUnitPage({policy, route}: WorkspaceRateAndUnitPageProps) { - const {translate, toLocaleDigit} = useLocalize(); - const styles = useThemeStyles(); - - useEffect(() => { - if ((policy?.customUnits ?? []).length !== 0) { - return; - } - - BankAccounts.setReimbursementAccountLoading(true); - Policy.openWorkspaceReimburseView(policy?.id ?? ''); - }, [policy?.customUnits, policy?.id]); - - const unitItems = [ - {label: translate('common.kilometers'), value: CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS}, - {label: translate('common.miles'), value: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, - ]; - - const saveUnitAndRate = (unit: Unit, rate: string) => { - const distanceCustomUnit = Object.values(policy?.customUnits ?? {}).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); - if (!distanceCustomUnit) { - return; - } - const currentCustomUnitRate = Object.values(distanceCustomUnit?.rates ?? {}).find((r) => r.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); - const unitID = distanceCustomUnit.customUnitID ?? ''; - const unitName = distanceCustomUnit.name ?? ''; - const rateNumValue = PolicyUtils.getNumericValue(rate, toLocaleDigit); - - const newCustomUnit: Policy.NewCustomUnit = { - customUnitID: unitID, - name: unitName, - attributes: {unit}, - rates: { - ...currentCustomUnitRate, - rate: Number(rateNumValue) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, - }, - }; - - Policy.updateWorkspaceCustomUnitAndRate(policy?.id ?? '', distanceCustomUnit, newCustomUnit, policy?.lastModified); - }; - - const submit = (values: FormOnyxValues) => { - saveUnitAndRate(values.unit as Unit, values.rate); - Keyboard.dismiss(); - Navigation.goBack(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy?.id ?? '')); - }; - - const validate = (values: FormOnyxValues): ValidationError => { - const errors: ValidationError = {}; - const decimalSeparator = toLocaleDigit('.'); - const outputCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; - // Allow one more decimal place for accuracy - const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,${CurrencyUtils.getCurrencyDecimals(outputCurrency) + 1}})?$`, 'i'); - if (!rateValueRegex.test(values.rate) || values.rate === '') { - errors.rate = 'workspace.reimburse.invalidRateError'; - } else if (NumberUtils.parseFloatAnyLocale(values.rate) <= 0) { - errors.rate = 'workspace.reimburse.lowRateError'; - } - return errors; - }; - - const distanceCustomUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); - const distanceCustomRate = Object.values(distanceCustomUnit?.rates ?? {}).find((rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); - - return ( - - {() => ( - - Policy.clearCustomUnitErrors(policy?.id ?? '', distanceCustomUnit?.customUnitID ?? '', distanceCustomRate?.customUnitRateID ?? '')} - > - - - - - - - - )} - - ); -} - -WorkspaceRateAndUnitPage.displayName = 'WorkspaceRateAndUnitPage'; - -export default withPolicy(WorkspaceRateAndUnitPage); diff --git a/src/styles/index.ts b/src/styles/index.ts index 54a72bee54eb..419cc5a3fd4a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -799,6 +799,7 @@ const styles = (theme: ThemeColors) => fontSize: 17, }, modalViewMiddle: { + position: 'relative', backgroundColor: theme.border, borderTopWidth: 0, }, @@ -841,6 +842,10 @@ const styles = (theme: ThemeColors) => width: variables.iconSizeExtraSmall, height: variables.iconSizeExtraSmall, }, + chevronContainer: { + pointerEvents: 'none', + opacity: 0, + }, } satisfies CustomPickerStyle), badge: { From 8fe5659453ece00df5096f0b3b16800c5735e61f Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 6 May 2024 10:55:32 -0400 Subject: [PATCH 122/219] Update redirects.csv https://github.com/Expensify/App/pull/41675 --- docs/redirects.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index f775d2f97094..c3d6020bbc83 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -170,3 +170,4 @@ https://help.expensify.com/articles/expensify-classic/reports/Report-Audit-Log-a https://help.expensify.com/articles/expensify-classic/reports/The-Reports-Page,https://help.expensify.com/articles/expensify-classic/reports/Report-statuses https://help.expensify.com/articles/new-expensify/getting-started/Free-plan-upgrade-to-collect-plan,https://help.expensify.com/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account,https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account +https://help.expensify.com/articles/new-expensify/settings/Profile,https://help.expensify.com/new-expensify/hubs/settings/ From dd86c65fd3f7930ea637b9723de81346ee6cc1cd Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Mon, 6 May 2024 23:50:12 +0530 Subject: [PATCH 123/219] fix: added flex1 to container --- .../accounting/xero/XeroMapCostCentersToConfigurationPage.tsx | 1 + .../accounting/xero/XeroMapRegionsToConfigurationPage.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 4280bcedbf3e..160f8f5554a6 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -54,6 +54,7 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { policyID={policyID && category?.id ? policyID : ''} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} titleStyle={[styles.pb2, styles.ph5]} + contentContainerStyle={[styles.flex1]} > Date: Mon, 6 May 2024 13:33:26 -0600 Subject: [PATCH 124/219] create getSearchType, use const --- src/components/Search.tsx | 10 ++++++++-- src/libs/SearchUtils.ts | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 667ed3dbe6f2..fbd352a798c6 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -4,13 +4,13 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import Log from '@libs/Log'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import EmptySearchView from '@pages/Search/EmptySearchView'; import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {SearchDataTypes} from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import SelectionList from './SelectionList'; @@ -56,7 +56,13 @@ function Search({query}: SearchProps) { Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(query, reportID)); }; - const type = searchResults?.search?.type as SearchDataTypes; + const type = SearchUtils.getSearchType(searchResults?.search); + + if (type === undefined) { + Log.alert('[Search] Undefined search type'); + return null; + } + const ListItem = SearchUtils.getListItem(type); const data = SearchUtils.getSections(searchResults?.data ?? {}, type); const shouldShowMerchant = SearchUtils.getShouldShowMerchant(searchResults?.data ?? {}); diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index a4c5d13f57fd..55e81a717bac 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -2,9 +2,18 @@ import TransactionListItem from '@components/SelectionList/TransactionListItem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {SearchTransaction, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; +import type {SearchDataTypes, SearchTransaction, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; import * as UserUtils from './UserUtils'; +function getSearchType(search: OnyxTypes.SearchResults['search']): SearchDataTypes | undefined { + switch (search.type) { + case CONST.SEARCH_DATA_TYPES.TRANSACTION: + return CONST.SEARCH_DATA_TYPES.TRANSACTION; + default: + return undefined; + } +} + function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { return Object.values(data).some((item) => { const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? ''; @@ -34,7 +43,7 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): SearchT } const searchTypeToItemMap: SearchTypeToItemMap = { - transaction: { + [CONST.SEARCH_DATA_TYPES.TRANSACTION]: { listItem: TransactionListItem, getSections: getTransactionsSections, }, @@ -52,4 +61,4 @@ function getQueryHash(query: string): number { return UserUtils.hashText(query, 2 ** 32); } -export {getListItem, getQueryHash, getSections, getShouldShowMerchant}; +export {getListItem, getQueryHash, getSections, getShouldShowMerchant, getSearchType}; From ea663b597f0670048d0d7de892cd5ba8e4ba1ca4 Mon Sep 17 00:00:00 2001 From: Rory Abraham <47436092+roryabraham@users.noreply.github.com> Date: Mon, 6 May 2024 12:44:37 -0700 Subject: [PATCH 125/219] Revert "[CP Staging] fix: remove sensor animation for now" --- .../report/AnimatedEmptyStateBackground.tsx | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/AnimatedEmptyStateBackground.tsx b/src/pages/home/report/AnimatedEmptyStateBackground.tsx index 7ddbdaef687b..4199ebe5937f 100644 --- a/src/pages/home/report/AnimatedEmptyStateBackground.tsx +++ b/src/pages/home/report/AnimatedEmptyStateBackground.tsx @@ -1,11 +1,15 @@ import React from 'react'; -import {Image} from 'react-native'; +import Animated, {clamp, SensorType, useAnimatedSensor, useAnimatedStyle, useReducedMotion, useSharedValue, withSpring} from 'react-native-reanimated'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useWindowDimensions from '@hooks/useWindowDimensions'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +// Maximum horizontal and vertical shift in pixels on sensor value change +const IMAGE_OFFSET_X = 30; +const IMAGE_OFFSET_Y = 20; + function AnimatedEmptyStateBackground() { const StyleUtils = useStyleUtils(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -14,10 +18,36 @@ function AnimatedEmptyStateBackground() { // If window width is greater than the max background width, repeat the background image const maxBackgroundWidth = variables.sideBarWidth + CONST.EMPTY_STATE_BACKGROUND.ASPECT_RATIO * CONST.EMPTY_STATE_BACKGROUND.WIDE_SCREEN.IMAGE_HEIGHT; + // Get data from phone rotation sensor and prep other variables for animation + const animatedSensor = useAnimatedSensor(SensorType.GYROSCOPE); + const xOffset = useSharedValue(0); + const yOffset = useSharedValue(0); + const isReducedMotionEnabled = useReducedMotion(); + + // Apply data to create style object + const animatedStyles = useAnimatedStyle(() => { + if (!isSmallScreenWidth || isReducedMotionEnabled) { + return {}; + } + /* + * We use x and y gyroscope velocity and add it to position offset to move background based on device movements. + * Position the phone was in while entering the screen is the initial position for background image. + */ + const {x, y} = animatedSensor.sensor.value; + // The x vs y here seems wrong but is the way to make it feel right to the user + xOffset.value = clamp(xOffset.value + y * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_X, IMAGE_OFFSET_X); + yOffset.value = clamp(yOffset.value - x * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_Y, IMAGE_OFFSET_Y); + return { + // On Android, scroll view sub views gets clipped beyond container bounds. Set the top position so that image wouldn't get clipped + top: IMAGE_OFFSET_Y, + transform: [{translateX: withSpring(xOffset.value)}, {translateY: withSpring(yOffset.value, {overshootClamping: true})}, {scale: 1.15}], + }; + }, [isReducedMotionEnabled]); + return ( - maxBackgroundWidth ? 'repeat' : 'cover'} /> ); From 2745a86922936e0e1a78514501415302f1039a0c Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Mon, 6 May 2024 12:56:41 -0700 Subject: [PATCH 126/219] Add markAsCash action for dismissing the rter violation --- src/libs/API/parameters/MarkAsCashParams.ts | 5 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/ReportUtils.ts | 38 ++++++++++++ src/libs/actions/Transaction.ts | 64 +++++++++++++++++++-- 5 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/libs/API/parameters/MarkAsCashParams.ts diff --git a/src/libs/API/parameters/MarkAsCashParams.ts b/src/libs/API/parameters/MarkAsCashParams.ts new file mode 100644 index 000000000000..2bf07afe730c --- /dev/null +++ b/src/libs/API/parameters/MarkAsCashParams.ts @@ -0,0 +1,5 @@ +type MarkAsCashParams = { + transactionID: string; +} + +export default MarkAsCashParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 30261051c0e5..4e9aaf2a1767 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -219,3 +219,4 @@ export type {default as LeavePolicyParams} from './LeavePolicyParams'; export type {default as OpenPolicyAccountingPageParams} from './OpenPolicyAccountingPageParams'; export type {default as SearchParams} from './Search'; export type {default as SendInvoiceParams} from './SendInvoiceParams'; +export type {default as MarkAsCashParams} from './MarkAsCashParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c61e1278ff8a..226b0ba2ec41 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -215,6 +215,7 @@ const WRITE_COMMANDS = { LEAVE_POLICY: 'LeavePolicy', ACCEPT_SPOTNANA_TERMS: 'AcceptSpotnanaTerms', SEND_INVOICE: 'SendInvoice', + MARK_AS_CASH: 'MarkAsCash', } as const; type WriteCommand = ValueOf; @@ -430,6 +431,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams; [WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: EmptyObject; [WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams; + [WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams; }; const READ_COMMANDS = { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8dd04d168717..efe1c69decf4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -42,6 +42,7 @@ import type { IOUMessage, OriginalMessageActionName, OriginalMessageCreated, + OriginalMessageDismissedViolation, OriginalMessageReimbursementDequeued, OriginalMessageRenamed, OriginalMessageRoomChangeLog, @@ -254,6 +255,11 @@ type OptimisticClosedReportAction = Pick< 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'pendingAction' | 'person' | 'reportActionID' | 'shouldShow' >; +type OptimisticDismissedViolationReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' +>; + type OptimisticCreatedReportAction = OriginalMessageCreated & Pick; @@ -4624,6 +4630,37 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam }; } +/** + * Returns an optimistic Dismissed Violation Report Action. Use the originalMessage customize this to the type of + * violation being dismissed. + */ +function buildOptimisticDismissedViolationReportAction(originalMessage: OriginalMessageDismissedViolation['originalMessage']): OptimisticDismissedViolationReportAction { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION, + actorAccountID: currentUserAccountID, + avatar: getCurrentUserAvatarOrDefault(), + created: DateUtils.getDBTime(), + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: ReportActionsUtils.getDismissedViolationMessageText(originalMessage), + }, + ], + originalMessage, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + } + ], + reportActionID: NumberUtils.rand64(), + shouldShow: true, + } +} + function buildOptimisticWorkspaceChats(policyID: string, policyName: string): OptimisticWorkspaceChats { const announceChatData = buildOptimisticChatReport( currentUserAccountID ? [currentUserAccountID] : [], @@ -6467,6 +6504,7 @@ export { buildOptimisticChatReport, buildOptimisticClosedReportAction, buildOptimisticCreatedReportAction, + buildOptimisticDismissedViolationReportAction, buildOptimisticEditedTaskFieldReportAction, buildOptimisticExpenseReport, buildOptimisticGroupChatReport, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index a95bf9a825f0..9e0135f473c7 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -4,15 +4,16 @@ import lodashHas from 'lodash/has'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {GetRouteParams} from '@libs/API/parameters'; -import {READ_COMMANDS} from '@libs/API/types'; +import type {GetRouteParams, MarkAsCashParams} from '@libs/API/parameters'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {RecentWaypoint, Transaction} from '@src/types/onyx'; +import type {RecentWaypoint, Transaction, TransactionViolation} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; +import {buildOptimisticDismissedViolationReportAction} from "@libs/ReportUtils"; let recentWaypoints: RecentWaypoint[] = []; Onyx.connect({ @@ -32,6 +33,14 @@ Onyx.connect({ }, }); +let currentUserEmail = ''; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + currentUserEmail = value?.email ?? ''; + }, +}); + function createInitialWaypoints(transactionID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { comment: { @@ -264,4 +273,51 @@ function clearError(transactionID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null}); } -export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, updateWaypoints, clearError}; +function markAsCash(transactionID: string, transactionThreadReportID: string, existingViolations: TransactionViolation[]) { + const optimisticReportAction = buildOptimisticDismissedViolationReportAction({ + reason: 'manual', + violationName: CONST.VIOLATIONS.RTER, + }); + const optimisticReportActions = { + [optimisticReportAction.reportActionID]: optimisticReportAction, + } + const onyxData: OnyxData = { + optimisticData: [ + // Optimistically dismissing the violation, removing it from the list of violations + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: existingViolations.filter((violation: TransactionViolation) => violation.name !== CONST.VIOLATIONS.RTER), + }, + // Optimistically adding the system message indicating we dismissed the violation + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: optimisticReportActions as ReportActions + }, + ], + failureData: [ + // Rolling back the dismissal of the violation + { + onyxMethod: Onyx.METHOD.MERGE, + key: `ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS${transactionID}`, + value: existingViolations, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: { + [optimisticReportAction.reportActionID]: null, + }, + }, + ], + }; + + const parameters: MarkAsCashParams = { + transactionID, + }; + + return API.write(WRITE_COMMANDS.MARK_AS_CASH, parameters, onyxData); +} + +export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, updateWaypoints, clearError, markAsCash}; From eb89605c4cf95c456895e13db9c24859e82a74b4 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 6 May 2024 14:16:32 -0600 Subject: [PATCH 127/219] fix lint --- src/pages/Search/SearchPage.tsx | 2 +- src/types/onyx/SearchResults.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index cd744287664c..1717e35e505e 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -12,7 +12,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {SearchQuery} from '@src/types/onyx/SearchResults'; -import IconAsset from '@src/types/utils/IconAsset'; +import type IconAsset from '@src/types/utils/IconAsset'; type SearchPageProps = StackScreenProps; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 589bb876b933..ead7cc591fe5 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,5 +1,5 @@ import type {ValueOf} from 'type-fest'; -import TransactionListItem from '@components/SelectionList/TransactionListItem'; +import type TransactionListItem from '@components/SelectionList/TransactionListItem'; import type CONST from '@src/CONST'; import type {Receipt} from './Transaction'; From a4aa909383980dfca1e0c461507981bb6868d3c8 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Mon, 6 May 2024 16:09:25 -0700 Subject: [PATCH 128/219] Send optimistic report actionID --- src/libs/API/parameters/MarkAsCashParams.ts | 1 + src/libs/actions/Transaction.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/libs/API/parameters/MarkAsCashParams.ts b/src/libs/API/parameters/MarkAsCashParams.ts index 2bf07afe730c..89c02d979e09 100644 --- a/src/libs/API/parameters/MarkAsCashParams.ts +++ b/src/libs/API/parameters/MarkAsCashParams.ts @@ -1,5 +1,6 @@ type MarkAsCashParams = { transactionID: string; + reportActionID: string; } export default MarkAsCashParams; diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 9e0135f473c7..d01802ec4b00 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -315,6 +315,7 @@ function markAsCash(transactionID: string, transactionThreadReportID: string, ex const parameters: MarkAsCashParams = { transactionID, + reportActionID: optimisticReportAction.reportActionID }; return API.write(WRITE_COMMANDS.MARK_AS_CASH, parameters, onyxData); From dc54b1afca944a990e0821bbb19e017318dd8929 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Mon, 6 May 2024 16:20:09 -0700 Subject: [PATCH 129/219] Fix typo omission --- src/libs/actions/Transaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index d01802ec4b00..40559219a139 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -10,7 +10,7 @@ import * as CollectionUtils from '@libs/CollectionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {RecentWaypoint, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {RecentWaypoint, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; import {buildOptimisticDismissedViolationReportAction} from "@libs/ReportUtils"; @@ -300,7 +300,7 @@ function markAsCash(transactionID: string, transactionThreadReportID: string, ex // Rolling back the dismissal of the violation { onyxMethod: Onyx.METHOD.MERGE, - key: `ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS${transactionID}`, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, value: existingViolations, }, { From f4839f8c0388d9ad914bb6c9d966d869ef646c2a Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Mon, 6 May 2024 16:20:50 -0700 Subject: [PATCH 130/219] prettier --- src/libs/API/parameters/MarkAsCashParams.ts | 2 +- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/Transaction.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/API/parameters/MarkAsCashParams.ts b/src/libs/API/parameters/MarkAsCashParams.ts index 89c02d979e09..751963f1a52f 100644 --- a/src/libs/API/parameters/MarkAsCashParams.ts +++ b/src/libs/API/parameters/MarkAsCashParams.ts @@ -1,6 +1,6 @@ type MarkAsCashParams = { transactionID: string; reportActionID: string; -} +}; export default MarkAsCashParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 50cd43ede80f..748b4389604e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4675,11 +4675,11 @@ function buildOptimisticDismissedViolationReportAction(originalMessage: Original type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', text: getCurrentUserDisplayNameOrEmail(), - } + }, ], reportActionID: NumberUtils.rand64(), shouldShow: true, - } + }; } function buildOptimisticWorkspaceChats(policyID: string, policyName: string, expenseReportId?: string): OptimisticWorkspaceChats { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 40559219a139..2afbcd9e6ef6 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -7,13 +7,13 @@ import * as API from '@libs/API'; import type {GetRouteParams, MarkAsCashParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; +import {buildOptimisticDismissedViolationReportAction} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; -import {buildOptimisticDismissedViolationReportAction} from "@libs/ReportUtils"; let recentWaypoints: RecentWaypoint[] = []; Onyx.connect({ @@ -280,7 +280,7 @@ function markAsCash(transactionID: string, transactionThreadReportID: string, ex }); const optimisticReportActions = { [optimisticReportAction.reportActionID]: optimisticReportAction, - } + }; const onyxData: OnyxData = { optimisticData: [ // Optimistically dismissing the violation, removing it from the list of violations @@ -293,7 +293,7 @@ function markAsCash(transactionID: string, transactionThreadReportID: string, ex { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, - value: optimisticReportActions as ReportActions + value: optimisticReportActions as ReportActions, }, ], failureData: [ @@ -315,7 +315,7 @@ function markAsCash(transactionID: string, transactionThreadReportID: string, ex const parameters: MarkAsCashParams = { transactionID, - reportActionID: optimisticReportAction.reportActionID + reportActionID: optimisticReportAction.reportActionID, }; return API.write(WRITE_COMMANDS.MARK_AS_CASH, parameters, onyxData); From fd662c2a0ff9ca9da9b96eaf61894b275d76e7be Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Mon, 6 May 2024 16:29:22 -0700 Subject: [PATCH 131/219] Remove unneccessary onyx subscriber --- src/libs/actions/Transaction.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 2afbcd9e6ef6..f403ed5a86bc 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -33,14 +33,6 @@ Onyx.connect({ }, }); -let currentUserEmail = ''; -Onyx.connect({ - key: ONYXKEYS.SESSION, - callback: (value) => { - currentUserEmail = value?.email ?? ''; - }, -}); - function createInitialWaypoints(transactionID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { comment: { From c39e9eaa906033bbcc5935c1b9e92e8b915b5e22 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Tue, 7 May 2024 06:20:41 +0530 Subject: [PATCH 132/219] Add style to QBO Setup Button --- .../ConnectToQuickbooksOnlineButton/index.native.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index 3a5e545cce88..212674e2a125 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -9,6 +9,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline'; import CONST from '@src/CONST'; @@ -29,6 +30,7 @@ function ConnectToQuickbooksOnlineButton({ shouldDisconnectIntegrationBeforeConnecting, integrationToDisconnect, }: ConnectToQuickbooksOnlineButtonProps & ConnectToQuickbooksOnlineButtonOnyxProps) { + const styles = useThemeStyles(); const {translate} = useLocalize(); const webViewRef = useRef(null); const [isWebViewOpen, setWebViewOpen] = useState(false); @@ -48,6 +50,7 @@ function ConnectToQuickbooksOnlineButton({ setWebViewOpen(true); }} text={translate('workspace.accounting.setup')} + style={styles.justifyContentCenter} small /> {shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && ( From 79cc3568b32dfcfadb2f1e3b5392d36727ddf1de Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Mon, 6 May 2024 19:54:38 -0500 Subject: [PATCH 133/219] DOCS: Create Set-up-QuickBooks-Online-connection.md New article --- .../Set-up-QuickBooks-Online-connection.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md diff --git a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md new file mode 100644 index 000000000000..772c38c8305c --- /dev/null +++ b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md @@ -0,0 +1,126 @@ +--- +title: Set up QuickBooks online connection +description: Integrate QuickBooks Online with Expensify +--- +
+ +{% include info.html %} +To use the QuickBooks Online connection, you must have a QuickBooks Online account and an Expensify Collect plan. The QuickBooks Self-employed subscription is not supported. +{% include end-info.html %} + +The features available for the Expensify connection with QuickBooks Online vary based on your QuickBooks subscription. The features may still be visible in Expensify even if you don’t have access, but you will receive an error if the feature isn't available with your subscription. + +Here is a list of the features supported by each QuickBooks Online subscription: + +| Feature | Simple Start | Essentials | Essentials Plus | +|----------------------------|--------------|------------|-----------------| +| Expense Reports | ✔ | ✔ | ✔ | +| GL Accounts as Categories | ✔ | ✔ | ✔ | +| Credit Card Transactions | ✔ | ✔ | ✔ | +| Debit Card Transaction | | ✔ | ✔ | +| Classes | | ✔ | ✔ | +| Customers | | ✔ | ✔ | +| Projects | | ✔ | ✔ | +| Vendor Bills | | ✔ | ✔ | +| Journal Entries | | ✔ | ✔ | +| Tax | | ✔ | ✔ | +| Billable | | | ✔ | +| Location | | | ✔ | + +To set up your QuickBooks Online connection, complete the 5 steps below. + +# Step 1: Set up employees in QuickBooks Online + +Log in to QuickBooks Online and ensure all of your employees are setup as either Vendors or Employees using the same email address that they are listed under in Expensify. This process may vary by country, but you can go to **Payroll** and select **Employees** in QuickBooks Online to add new employees or edit existing ones. + +# Step 2: Connect Expensify to QuickBooks Online + +
    +
  1. Click your profile image or icon in the bottom left menu.
  2. +
  3. Scroll down and click Workspaces in the left menu.
  4. +
  5. Select the workspace you want to book the travel under.
  6. +
  7. Click More features in the left menu.
  8. +
  9. Scroll down to the Integrate section and enable the Accounting toggle.
  10. +
  11. Click Accounting in the left menu.
  12. +
  13. Click Set up to the right of QuickBooks Online.
  14. +
  15. Enter your Intuit login details to import your settings from QuickBooks Online to Expensify.
  16. +
+ +# Step 3: Configure import settings + +The following steps help you determine how data will be imported from QuickBooks Online to Expensify. + +
    +
  1. Under the Accounting settings for your workspace, click Import under the QuickBooks Online connection.
  2. +
  3. Review each of the following import settings:
  4. +
      +
    • Chart of accounts: The chart of accounts are automatically imported from QuickBooks Online as categories. This cannot be amended.
    • +
    • Classes: Choose whether to import classes, which will be shown in Expensify as tags for expense-level coding.
    • +
    • Customers/projects: Choose whether to import customers/projects, which will be shown in Expensify as tags for expense-level coding.
    • +
    • Locations: Choose whether to import locations, which will be shown in Expensify as tags for expense-level coding.
    • +{% include info.html %} +As Locations are only configurable as tags, you cannot export expense reports as vendor bills or checks to QuickBooks Online. To unlock these export options, either disable locations import or upgrade to the Control Plan to export locations encoded as a report field. +{% include end-info.html %} +
    • Taxes: Choose whether to import tax rates and defaults.
    • +
    +
+ +# Step 4: Configure export settings + +The following steps help you determine how data will be exported from Expensify to QuickBooks Online. + +
    +
  1. Under the Accounting settings for your workspace, click Export under the QuickBooks Online connection.
  2. +
  3. Review each of the following export settings:
  4. +
      +
    • Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
    • + +{% include info.html %} +* Other Workspace Admins will still be able to export to QuickBooks Online. +* If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin. +{% include end-info.html %} + +
    • Date: Choose whether to use the date of last expense, export date, or submitted date.
    • +
    • Export Out-of-Pocket Expenses as: Select whether out-of-pocket expenses will be exported as a check, journal entry, or vendor bill.
    • + +{% include info.html %} +These settings may vary based on whether tax is enabled for your workspace. +* If tax is not enabled on the workspace, you’ll also select the Accounts Payable/AP. +* If tax is enabled on the workspace, journal entry will not be available as an option. If you select the journal entries option first and later enable tax on the workspace, you will see a red dot and an error message under the “Export Out-of-Pocket Expenses as” options. To resolve this error, you must change your export option to vendor bill or check to successfully code and export expense reports. +{% include end-info.html %} + +
    • Invoices: Select the QuickBooks Online invoice account that invoices will be exported to.
    • +
    • Export as: Select whether company cards export to QuickBooks Online as a credit card (the default), debit card, or vendor bill. Then select the account they will export to.
    • +
    • If you select vendor bill, you’ll also select the accounts payable account that vendor bills will be created from, as well as whether to set a default vendor for credit card transactions upon export. If this option is enabled, you will select the vendor that all credit card transactions will be applied to.
    • +
    +
+ +# Step 5: Configure advanced settings + +The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings. + +
    +
  1. Under the Accounting settings for your workspace, click Advanced under the QuickBooks Online connection.
  2. +
  3. Select an option for each of the following settings:
  4. +
      +
    • Auto-sync: Choose whether to enable QuickBooks Online to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period.
    • +
    • Invite Employees: Choose whether to enable Expensify to import employee records from QuickBooks Online and invite them to this workspace.
    • +
    • Automatically Create Entities: Choose whether to enable Expensify to automatically create vendors and customers in QuickBooks Online if a matching vendor or customer does not exist.
    • +
    • Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in QuickBooks Online will also show in Expensify as Paid. If enabled, you must also select the QuickBooks Online account that reimbursements are coming out of, and Expensify will automatically create the payment in QuickBooks Online.
    • +
    • Invoice Collection Account: Select the invoice collection account that you want invoices to appear under once the invoice is marked as paid.
    • +
    +
+ +{% include faq-begin.md %} + +**Why do I see a red dot next to my connection?** +If there is an error with your connection, you’ll see a red dot next to Accounting in the left menu. When you click Accounting, you’ll also see a red dot displayed next to the QuickBooks Online connection card. + +This may occur if you incorrectly enter your QuickBooks Online login information when trying to establish the connection. To resubmit your login details, +1. Click the three-dot menu to the right of the QuickBooks Online connection. +2. Click **Enter credentials**. +3. Enter your Intuit login details (the login information you use for QuickBooks Online) to establish the connection. + +{% include faq-end.md %} + +
From e5b51f4a85d09709ea5a6e71c11972c14c05c5bb Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 7 May 2024 10:22:19 +0900 Subject: [PATCH 134/219] imports --- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 55a692495f2d..a0e1caf5db5e 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -21,8 +21,6 @@ import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRo import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import {getDisplayNameForParticipant} from '@libs/ReportUtils'; -import {isArchivedRoom} from '@libs/ReportUtils'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy'; @@ -200,7 +198,7 @@ function FloatingActionButtonAndPopover( return ''; } if (quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && quickActionAvatars.length > 0) { - const name: string = getDisplayNameForParticipant(+(quickActionAvatars[0]?.id ?? 0), true) ?? ''; + const name: string = ReportUtils.getDisplayNameForParticipant(+(quickActionAvatars[0]?.id ?? 0), true) ?? ''; return translate('quickAction.paySomeone', {name}); } const titleKey = getQuickActionTitle(quickAction?.action ?? ('' as QuickActionName)); @@ -219,7 +217,7 @@ function FloatingActionButtonAndPopover( }, [personalDetails, quickActionReport, quickAction?.action, quickActionAvatars]); const navigateToQuickAction = () => { - const isValidReport = !(isEmptyObject(quickActionReport) || isArchivedRoom(quickActionReport)); + const isValidReport = !(isEmptyObject(quickActionReport) || ReportUtils.isArchivedRoom(quickActionReport)); const quickActionReportID = isValidReport ? quickActionReport?.reportID ?? '' : ReportUtils.generateReportID(); switch (quickAction?.action) { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: From 63af2a1902b1d140695f2247b041e7f3f089e275 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Mon, 6 May 2024 20:28:54 -0500 Subject: [PATCH 135/219] DOCS: Create Switch-to-light-or-dark-mode.md New article --- .../settings/Switch-to-light-or-dark-mode.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md diff --git a/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md b/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md new file mode 100644 index 000000000000..7a0e4900cc4c --- /dev/null +++ b/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md @@ -0,0 +1,30 @@ +--- +title: Switch to light or dark mode +description: Change the appearance of Expensify +--- +
+ +Expensify has three themes that determine how the app looks: +- **Dark mode**: The app appears with a black background +- **Light mode**: The app appears with a white background +- **Use Device settings**: Expensify will automatically use your device’s default theme + +To change your Expensify theme, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Preferences** in the left menu. +3. Click the **Theme** option and select the desired theme. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Preferences**. +3. Tap the **Theme** option and select the desired theme. +{% include end-option.html %} + +{% include end-selector.html %} + +
From 60aae42e3f252336f971715541383ff90a2ee51f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 7 May 2024 02:53:28 +0100 Subject: [PATCH 136/219] Fix row being focused --- src/components/MoneyRequestConfirmationList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b97578210ad9..ffb996ea9d10 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1090,6 +1090,7 @@ function MoneyRequestConfirmationList({ onConfirmSelection={confirm} selectedOptions={selectedOptions} disableArrowKeysActions + disableFocusOptions boldStyle showTitleTooltip shouldTextInputAppearBelowOptions From 5b4415ca90c7fdc973a438ddbe76e1db86d51950 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 7 May 2024 03:15:50 +0100 Subject: [PATCH 137/219] Fix bug when changing participants --- src/libs/actions/IOU.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 79ee20971e5d..7171a7d6732a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -6423,9 +6423,8 @@ function setSplitShares(transaction: OnyxEntry, amount: n } const isPayer = accountID === userAccountID; - - // This function expects the length of participants without current user - const splitAmount = IOUUtils.calculateAmount(accountIDs.length - 1, amount, currency, isPayer); + const participantsLength = newAccountIDs.includes(userAccountID) ? newAccountIDs.length - 1 : newAccountIDs.length; + const splitAmount = IOUUtils.calculateAmount(participantsLength, amount, currency, isPayer); return { ...result, [accountID]: { From c9703417e8446611c37cef56f38f3bbf2282795d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 7 May 2024 10:39:14 +0800 Subject: [PATCH 138/219] rework the function --- src/components/Modal/BaseModal.tsx | 2 +- src/libs/actions/Modal.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 89b7afce617d..b89d576d7274 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -75,7 +75,7 @@ function BaseModal( */ const hideModal = useCallback( (callHideCallback = true) => { - if (Modal.getModalVisibleCount() === 0) { + if (Modal.areAllModalsHidden()) { Modal.willAlertModalBecomeVisible(false); if (shouldSetModalVisibility) { Modal.setModalVisibility(false); diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index 5b907655cc3b..9cba7a359537 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -85,8 +85,8 @@ function willAlertModalBecomeVisible(isVisible: boolean, isPopover = false) { Onyx.merge(ONYXKEYS.MODAL, {willAlertModalBecomeVisible: isVisible, isPopover}); } -function getModalVisibleCount() { - return closeModals.length; +function areAllModalsHidden() { + return closeModals.length === 0; } -export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, setDisableDismissOnEscape, closeTop, getModalVisibleCount}; +export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, setDisableDismissOnEscape, closeTop, areAllModalsHidden}; From 51901d788cdfd70ae7ef051dfdc4e20cff4e68cd Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 7 May 2024 06:46:36 +0100 Subject: [PATCH 139/219] Fix focused state --- src/components/AmountTextInput.tsx | 6 +++++- src/components/MoneyRequestAmountInput.tsx | 6 ++++++ src/components/OptionRow.tsx | 4 ++-- src/components/TextInput/BaseTextInput/index.tsx | 1 + src/components/TextInputWithCurrencySymbol/types.ts | 4 ++++ src/styles/index.ts | 5 ----- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index e5980a397d37..52c32ce1f584 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -36,6 +36,9 @@ type AmountTextInputProps = { /** Style for the TextInput container */ containerStyle?: StyleProp; + + /** Hide the focus styles on TextInput */ + hideFocusedState?: boolean; } & Pick; function AmountTextInput( @@ -50,6 +53,7 @@ function AmountTextInput( onKeyPress, containerStyle, disableKeyboard = true, + hideFocusedState = true, ...rest }: AmountTextInputProps, ref: ForwardedRef, @@ -57,7 +61,7 @@ function AmountTextInput( return ( , @@ -279,6 +284,7 @@ function MoneyRequestAmountInput( prefixContainerStyle={props.prefixContainerStyle} touchableInputWrapperStyle={props.touchableInputWrapperStyle} maxLength={maxLength} + hideFocusedState={hideFocusedState} /> ); } diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 376e0113ca64..d00120a594d8 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -260,16 +260,16 @@ function OptionRow({ prefixCharacter={option.amountInputProps.prefixCharacter} disableKeyboard={false} isCurrencyPressable={false} + hideFocusedState={false} hideCurrencySymbol formatAmountOnBlur - touchableInputWrapperStyle={[styles.optionRowAmountInputWrapper, option.amountInputProps.containerStyle]} prefixContainerStyle={[styles.pv0]} + containerStyle={[styles.textInputContainer]} inputStyle={[ styles.optionRowAmountInput, StyleUtils.getPaddingLeft(StyleUtils.getCharacterPadding(option.amountInputProps.prefixCharacter ?? '') + styles.pl1.paddingLeft) as TextStyle, option.amountInputProps.inputStyle, ]} - containerStyle={styles.iouAmountTextInputContainer} onAmountChange={option.amountInputProps.onAmountChange} maxLength={option.amountInputProps.maxLength} /> diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index c73509aa7b8f..04d400424a87 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -296,6 +296,7 @@ function BaseTextInput( ]} > ; + /** Max length for the amount input */ maxLength?: number; + + /** Hide the focus styles on TextInput */ + hideFocusedState?: boolean; } & Pick; export default TextInputWithCurrencySymbolProps; diff --git a/src/styles/index.ts b/src/styles/index.ts index 4555e2e1001b..08874ecc55c7 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1137,11 +1137,6 @@ const styles = (theme: ThemeColors) => borderColor: theme.border, }, - optionRowAmountInputWrapper: { - borderColor: theme.border, - borderBottomWidth: 2, - }, - optionRowAmountInput: { textAlign: 'right', }, From b118e8c639d91aa06026f93c874150268e4dce0b Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Tue, 7 May 2024 12:48:13 +0530 Subject: [PATCH 140/219] fix: use view for map field --- .../accounting/xero/XeroMapCostCentersToConfigurationPage.tsx | 1 + .../accounting/xero/XeroMapRegionsToConfigurationPage.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx index 160f8f5554a6..b4f0fe04f6ce 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx @@ -55,6 +55,7 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} titleStyle={[styles.pb2, styles.ph5]} contentContainerStyle={[styles.flex1]} + shouldUseScrollView={false} > Date: Tue, 7 May 2024 14:47:12 +0700 Subject: [PATCH 141/219] fix perf test --- tests/perf-test/SidebarLinks.perf-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/perf-test/SidebarLinks.perf-test.tsx b/tests/perf-test/SidebarLinks.perf-test.tsx index 2848015d5c63..6018dca8dd24 100644 --- a/tests/perf-test/SidebarLinks.perf-test.tsx +++ b/tests/perf-test/SidebarLinks.perf-test.tsx @@ -21,7 +21,7 @@ const getMockedReportsMap = (length = 100) => { const reportID = index + 1; const participants = [1, 2]; const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; - const report = LHNTestUtils.getFakeReport(participants, 1, true); + const report = {...LHNTestUtils.getFakeReport(participants, 1, true), lastMessageText: 'hey'}; return [reportKey, report]; }), From 04a41dc252e8213fdfc462367116631ec596e5d9 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 7 May 2024 10:29:38 +0200 Subject: [PATCH 142/219] fix: more padding fixes --- src/pages/settings/AboutPage/ConsolePage.tsx | 2 +- src/pages/settings/Wallet/ReportCardLostPage.tsx | 1 - src/pages/settings/Wallet/TransferBalancePage.tsx | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index c9532fa041a0..132388365ada 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -154,7 +154,7 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { success text={translate('initialSettingsPage.debugConsole.execute')} onPress={executeArbitraryCode} - style={[styles.mt5]} + style={[styles.mv5]} large /> diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index 76cf54454b71..11790bd44cb6 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -203,7 +203,6 @@ function ReportCardLostPage({ onSubmit={handleSubmitFirstStep} message="reportCardLostOrDamaged.reasonError" buttonText={translate('reportCardLostOrDamaged.nextButtonLabel')} - containerStyles={[styles.m5]} /> )} diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx index 6f0e6ac016c7..c0f42fb9440d 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.tsx +++ b/src/pages/settings/Wallet/TransferBalancePage.tsx @@ -219,7 +219,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans isDisabled={isButtonDisabled || isOffline} message={errorMessage} isAlertVisible={!isEmptyObject(errorMessage)} - containerStyles={!paddingBottom ? styles.pb5 : null} + containerStyles={[styles.ph5, !paddingBottom ? styles.pb5 : null]} />
From 254f27455cd732e53c12be008b6f4c5dcfeeb55a Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Tue, 7 May 2024 11:05:59 +0200 Subject: [PATCH 143/219] remove hover pattern for scans, remove only visible to you --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/pages/home/report/ReportActionItem.tsx | 35 ++-------------------- 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d3bf538cbe89..6761025e19ea 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -482,7 +482,6 @@ export default { editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}?`, - onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', joinThread: 'Join thread', leaveThread: 'Leave thread', diff --git a/src/languages/es.ts b/src/languages/es.ts index f66add17af3f..30d323a5285a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -474,7 +474,6 @@ export default { deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}`, - onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', joinThread: 'Unirse al hilo', leaveThread: 'Dejar hilo', diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 84a689c6f03c..5024c03d5134 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -6,10 +6,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import Button from '@components/Button'; -import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import InlineSystemMessage from '@components/InlineSystemMessage'; import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -880,13 +877,8 @@ function ReportActionItem({ const hasErrors = !isEmptyObject(action.errors); const whisperedToAccountIDs = action.whisperedToAccountIDs ?? []; - const isWhisper = whisperedToAccountIDs.length > 0; - const isMultipleParticipant = whisperedToAccountIDs.length > 1; - const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); - const whisperedToPersonalDetails = isWhisper - ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) - : []; - const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; + const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(action.reportID); + const isWhisper = whisperedToAccountIDs.length > 0 && transactionsWithReceipts.length === 0; return ( - {isWhisper && ( - - - - - - {translate('reportActionContextMenu.onlyVisible')} -   - - - - )} {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)}
From c5c3c450d1720f7f99171b17eb4a21d4425990c2 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 7 May 2024 11:13:24 +0200 Subject: [PATCH 144/219] fix regression, type update --- src/libs/actions/Policy.ts | 2 +- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 2c08dd321b11..f861b899911c 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -282,7 +282,7 @@ function getPolicy(policyID: string | undefined): Policy | EmptyObject { /** * Returns a primary policy for the user */ -function getPrimaryPolicy(activePolicyID?: string): Policy | undefined { +function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefined { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); const primaryPolicy: Policy | null | undefined = allPolicies?.[activePolicyID ?? '']; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index e2965aba73ea..8f8e686b6a71 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -197,7 +197,7 @@ function MoneyRequestParticipantsSelector({ ]; if (iouType === CONST.IOU.TYPE.INVOICE) { - const primaryPolicy = Policy.getPrimaryPolicy(activePolicyID ?? undefined); + const primaryPolicy = Policy.getPrimaryPolicy(activePolicyID); newParticipants.push({ policyID: primaryPolicy?.id, @@ -276,7 +276,8 @@ function MoneyRequestParticipantsSelector({ // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet const isAllowedToSplit = - (canUseP2PDistanceRequests ?? iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && ![CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE].some((option) => option === iouType) && ![CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE].some((option) => option === action); From a407e7dd3eeb35ed676600b1fb9c71eb6c6f5db0 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 7 May 2024 11:23:08 +0200 Subject: [PATCH 145/219] update property to required --- .../iou/request/MoneyRequestParticipantsSelector.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 8f8e686b6a71..ec05c5ad6fda 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -45,17 +45,10 @@ type MoneyRequestParticipantsSelectorProps = { iouRequestType: IOURequestType; /** The action of the IOU, i.e. create, split, move */ - action?: IOUAction; + action: IOUAction; }; -function MoneyRequestParticipantsSelector({ - participants = [], - onFinish, - onParticipantsAdded, - iouType, - iouRequestType, - action = CONST.IOU.ACTION.CREATE, -}: MoneyRequestParticipantsSelectorProps) { +function MoneyRequestParticipantsSelector({participants = [], onFinish, onParticipantsAdded, iouType, iouRequestType, action}: MoneyRequestParticipantsSelectorProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); From b6f6e76a71c8145c36892e26285f0d7585101860 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 7 May 2024 12:47:29 +0200 Subject: [PATCH 146/219] apply suggested changes to STYLE.md --- contributingGuides/STYLE.md | 74 +------------------------------------ 1 file changed, 2 insertions(+), 72 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index ab64ec1667c6..41f924637c76 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -22,8 +22,6 @@ - [Satisfies Operator](#satisfies-operator) - [Type imports/exports](#type-importsexports) - [Refs](#refs) - - [Exception to Rules](#exception-to-rules) - - [Communication Items](#communication-items) - [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) - [Naming Conventions](#naming-conventions) - [Type names](#type-names) @@ -85,7 +83,7 @@ type Foo = { ### `d.ts` Extension -Do not use `d.ts` file extension even when a file contains only type declarations. Only exceptions are `src/types/global.d.ts` and `src/types/modules/*.d.ts` files in which third party packages and JavaScript's built-in modules (e.g. `window` object) can be modified using module augmentation. Refer to the [Communication Items](#communication-items) section to learn more about module augmentation. +Do not use `d.ts` file extension even when a file contains only type declarations. Only exceptions are `src/types/global.d.ts` and `src/types/modules/*.d.ts` files in which third party packages and JavaScript's built-in modules (e.g. `window` object) can be modified using module augmentation. > Why? Type errors in `d.ts` files are not checked by TypeScript. @@ -458,33 +456,6 @@ if (ref.current && 'getBoundingClientRect' in ref.current) { {#DO SOMETHING}}> ``` -### Exception to Rules - -Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily. - -When an exception is granted, link the relevant Slack conversation in your PR. Suppress ESLint or TypeScript warnings/errors with comments if necessary. - -This rule will apply until the migration is done. After the migration, discussion on granting exception can happen inside the PR page and doesn't need take place in the Slack channel. - -### Communication Items - -> Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. - -- I think types definitions in a third party library or JavaScript's built-in module are incomplete or incorrect - -When the library indeed contains incorrect or missing type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/types/modules/*.d.ts`, each library as a separate file. - -```ts -// external-library-name.d.ts - -declare module "external-library-name" { - interface LibraryComponentProps { - // Add or modify typings - additionalProp: string; - } -} -``` - ### Other Expensify Resources on TypeScript - [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) @@ -547,20 +518,6 @@ declare module "external-library-name" { } ``` - - Use {ComponentName}Handle for custon ref handle types. - - ```tsx - // BAD - type MyComponentRef = { - onPressed: () => void; - }; - - // GOOD - type MyComponentHandle = { - onPressed: () => void; - };s - ``` - - For generic type parameters, use `T` if you have only one type parameter. Don't use the `T`, `U`, `V`... sequence. Make type parameter names descriptive, each prefixed with `T`. > Prefix each type parameter name to distinguish them from other types. @@ -653,20 +610,6 @@ export { } ``` -Using named functions is the preferred way to write a callback method. - -```tsx -// Bad -people.map(function (item) {/* Long and complex logic */}); -people.map((item) => {/* Long and complex logic with many inner loops*/}); -useEffect/useMemo/useCallback(() => {/* Long and complex logic */}, []); - -// Good -function mappingPeople(person) {/* Long and complex logic */}; -people.map(mappingPeople); -useEffect/useMemo/useCallback(function handlingConnection() {/* Long and complex logic */}, []); -``` - You can still use arrow function for declarations or simple logics to keep them readable. ```tsx @@ -697,17 +640,6 @@ useEffect(() => { ``` -Empty functions (noop) should be declared as arrow functions with no whitespace inside. Avoid _.noop() - -```tsx -// Bad -const callback = _.noop; -const callback = () => { }; - -// Good -const callback = () => {}; -``` - ## `var`, `const` and `let` - Never use `var` @@ -733,7 +665,7 @@ if (someCondition) { ## Object / Array Methods -We have standardized on using the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods) instead of [underscore.js](https://underscorejs.org/) methods for objects and collections. As the vast majority of code is written in TypeScript, we can safaly use the native methods. +We have standardized on using the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods) instead of [lodash](https://lodash.com/) methods for objects and collections. As the vast majority of code is written in TypeScript, we can safely use the native methods. ```ts // Bad @@ -911,8 +843,6 @@ So, if a new language feature isn't something we have agreed to support it's off Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase: - **Async/Await** - Use the native `Promise` instead -- **Optional Chaining** - Yes, don't use `lodashGet()` -- **Null Coalescing Operator** - Yes, don't use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable ## React Coding Standards From 06904f13aff53c4b3680a7af65274611bce340e9 Mon Sep 17 00:00:00 2001 From: smelaa Date: Tue, 7 May 2024 14:46:19 +0200 Subject: [PATCH 147/219] Fix country selection bug --- src/libs/Navigation/types.ts | 5 ++++- .../workspace/WorkspaceProfileAddressPage.tsx | 14 +++++++++++++- src/pages/workspace/withPolicy.tsx | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 380ede9dfec5..4e8a7dde6c27 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -163,7 +163,10 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME]: undefined; [SCREENS.WORKSPACE.CURRENCY]: undefined; - [SCREENS.WORKSPACE.ADDRESS]: undefined; + [SCREENS.WORKSPACE.ADDRESS]: { + policyID: string; + country?: Country | ''; + }; [SCREENS.WORKSPACE.NAME]: undefined; [SCREENS.WORKSPACE.DESCRIPTION]: undefined; [SCREENS.WORKSPACE.SHARE]: undefined; diff --git a/src/pages/workspace/WorkspaceProfileAddressPage.tsx b/src/pages/workspace/WorkspaceProfileAddressPage.tsx index 175a3611ef57..35c0428a3d2d 100644 --- a/src/pages/workspace/WorkspaceProfileAddressPage.tsx +++ b/src/pages/workspace/WorkspaceProfileAddressPage.tsx @@ -4,11 +4,13 @@ import AddressForm from '@components/AddressForm'; import type {FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {updateAddress} from '@userActions/Policy'; import type {Country} from '@src/CONST'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {CompanyAddress} from '@src/types/onyx/Policy'; @@ -19,7 +21,7 @@ type WorkspaceProfileAddressPagePolicyProps = WithPolicyProps; type WorkspaceProfileAddressPageProps = StackScreenProps & WorkspaceProfileAddressPagePolicyProps; -function WorkspaceProfileAddressPage({policy}: WorkspaceProfileAddressPageProps) { +function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPageProps) { const {translate} = useLocalize(); const address = useMemo(() => policy?.address, [policy]); const [currentCountry, setCurrentCountry] = useState(address?.country); @@ -28,6 +30,9 @@ function WorkspaceProfileAddressPage({policy}: WorkspaceProfileAddressPageProps) const [city, setCity] = useState(address?.city); const [zipcode, setZipcode] = useState(address?.zipCode); + const countryFromUrlTemp = route?.params?.country; + const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : ''; + const updatePolicyAddress = (values: FormOnyxValues) => { if (!policy) { return; @@ -81,6 +86,13 @@ function WorkspaceProfileAddressPage({policy}: WorkspaceProfileAddressPageProps) setZipcode(address.zipCode); }, [address]); + useEffect(() => { + if (!countryFromUrl) { + return; + } + handleAddressChange(countryFromUrl, 'country'); + }, [countryFromUrl, handleAddressChange]); + return ( ; function getPolicyIDFromRoute(route: PolicyRoute): string { From 6c6ea5772bc6130853145359d350c9802c7bb9d8 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 7 May 2024 15:13:55 +0200 Subject: [PATCH 148/219] fix native gesture behaviour --- .../getOnboardingModalScreenOptions/index.native.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/getOnboardingModalScreenOptions/index.native.ts b/src/libs/Navigation/getOnboardingModalScreenOptions/index.native.ts index 170a2f1c5865..98dd131a9332 100644 --- a/src/libs/Navigation/getOnboardingModalScreenOptions/index.native.ts +++ b/src/libs/Navigation/getOnboardingModalScreenOptions/index.native.ts @@ -3,7 +3,10 @@ import type {ThemeStyles} from '@styles/index'; import type {StyleUtilsType} from '@styles/utils'; function getOnboardingModalScreenOptions(isSmallScreenWidth: boolean, styles: ThemeStyles, StyleUtils: StyleUtilsType) { - return getRootNavigatorScreenOptions(isSmallScreenWidth, styles, StyleUtils).fullScreen; + return { + ...getRootNavigatorScreenOptions(isSmallScreenWidth, styles, StyleUtils).fullScreen, + gestureEnabled: false, + }; } export default getOnboardingModalScreenOptions; From ded2ed8afc5e461128978e208f2c20b0baf331c0 Mon Sep 17 00:00:00 2001 From: smelaa Date: Tue, 7 May 2024 15:27:47 +0200 Subject: [PATCH 149/219] Fix workspace address visibility bug --- src/libs/Permissions.ts | 1 + src/pages/workspace/WorkspaceProfilePage.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 79955c0fdf30..0adaf3e3427b 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -37,6 +37,7 @@ function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { } function canUseSpotnanaTravel(betas: OnyxEntry): boolean { + return true; return !!betas?.includes(CONST.BETAS.SPOTNANA_TRAVEL) || canUseAllBetas(betas); } diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 02b41518533f..8500aecdcaa7 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -81,6 +81,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi ); const readOnly = !PolicyUtils.isPolicyAdmin(policy); const imageStyle: StyleProp = isSmallScreenWidth ? [styles.mhv12, styles.mhn5, styles.mbn5] : [styles.mhv8, styles.mhn8, styles.mbn5]; + const shouldShowAddress = !readOnly || formattedAddress; const DefaultAvatar = useCallback( () => ( @@ -221,7 +222,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi - {canUseSpotnanaTravel && ( + {canUseSpotnanaTravel && shouldShowAddress && ( Date: Tue, 7 May 2024 15:34:52 +0200 Subject: [PATCH 150/219] Restore spotnana beta visibility --- src/libs/Permissions.ts | 1 - src/pages/workspace/b | 0 2 files changed, 1 deletion(-) create mode 100644 src/pages/workspace/b diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 0adaf3e3427b..79955c0fdf30 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -37,7 +37,6 @@ function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { } function canUseSpotnanaTravel(betas: OnyxEntry): boolean { - return true; return !!betas?.includes(CONST.BETAS.SPOTNANA_TRAVEL) || canUseAllBetas(betas); } diff --git a/src/pages/workspace/b b/src/pages/workspace/b new file mode 100644 index 000000000000..e69de29bb2d1 From 64f10e848338ade0647be02c86615e5bbc010e4e Mon Sep 17 00:00:00 2001 From: smelaa Date: Tue, 7 May 2024 15:35:40 +0200 Subject: [PATCH 151/219] Clean up --- src/pages/workspace/b | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/pages/workspace/b diff --git a/src/pages/workspace/b b/src/pages/workspace/b deleted file mode 100644 index e69de29bb2d1..000000000000 From fe19eed611c766cf90c8b0baff433c80fcc2330d Mon Sep 17 00:00:00 2001 From: smelaa Date: Tue, 7 May 2024 16:08:32 +0200 Subject: [PATCH 152/219] eslint --- src/pages/workspace/WorkspaceProfileAddressPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceProfileAddressPage.tsx b/src/pages/workspace/WorkspaceProfileAddressPage.tsx index 35c0428a3d2d..b73a57c30223 100644 --- a/src/pages/workspace/WorkspaceProfileAddressPage.tsx +++ b/src/pages/workspace/WorkspaceProfileAddressPage.tsx @@ -4,7 +4,6 @@ import AddressForm from '@components/AddressForm'; import type {FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; From 22527c789c82d541331ad9bcf48e1517973b61a5 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Tue, 7 May 2024 21:42:47 +0530 Subject: [PATCH 153/219] fix: added isValidOption --- .../xero/XeroTrackingCategoryConfigurationPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 246e10611bcb..4043beb8b3ad 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -30,18 +30,20 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS)?.value ?? ''; const regionCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION)?.value ?? ''; if (costCenterCategoryValue) { + const isValidOption = Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).findIndex((option) => option.toLowerCase() === costCenterCategoryValue.toLowerCase()) > -1; availableCategories.push({ description: translate('workspace.xero.mapXeroCostCentersTo'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS.getRoute(policyID)), - title: translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths), + title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths): '', }); } if (regionCategoryValue) { + const isValidOption = Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).findIndex((option) => option.toLowerCase() === regionCategoryValue.toLowerCase()) > -1; availableCategories.push({ description: translate('workspace.xero.mapXeroRegionsTo'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION.getRoute(policyID)), - title: translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths), + title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths): '', }); } return availableCategories; From 3dc105dfd6ba2fe0c722445e4b365d102f02246f Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Tue, 7 May 2024 21:49:06 +0530 Subject: [PATCH 154/219] refactor: prettier fix --- .../accounting/xero/XeroTrackingCategoryConfigurationPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 4043beb8b3ad..195b93d3d73c 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -34,7 +34,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { availableCategories.push({ description: translate('workspace.xero.mapXeroCostCentersTo'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS.getRoute(policyID)), - title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths): '', + title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths) : '', }); } @@ -43,7 +43,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { availableCategories.push({ description: translate('workspace.xero.mapXeroRegionsTo'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION.getRoute(policyID)), - title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths): '', + title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths) : '', }); } return availableCategories; From f19732f37a1a00b4792df14f6682fa5bd433ebca Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Tue, 7 May 2024 18:52:04 +0200 Subject: [PATCH 155/219] fix: e2e tests flakiness (due to permission popup) --- tests/e2e/utils/installApp.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/utils/installApp.ts b/tests/e2e/utils/installApp.ts index dc6a9d64053f..82d0066c885b 100644 --- a/tests/e2e/utils/installApp.ts +++ b/tests/e2e/utils/installApp.ts @@ -19,7 +19,8 @@ export default function (packageName: string, path: string, platform = 'android' // Ignore errors Logger.warn('Failed to uninstall app:', error.message); }) + // install and grant push notifications permissions right away (the popup may block e2e tests sometimes) // eslint-disable-next-line @typescript-eslint/no-misused-promises - .finally(() => execAsync(`adb install ${path}`)) + .finally(() => execAsync(`adb install ${path}`).then(() => execAsync(`adb shell pm grant ${packageName.split('/')[0]} android.permission.POST_NOTIFICATIONS`))) ); } From aefe6c4ae11d895635f9ef1c35b4bb35e1dad40d Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 7 May 2024 22:22:06 +0530 Subject: [PATCH 156/219] fix: Expense - Console error shows up when opening date editor in transaction thread. Signed-off-by: Krishna Gupta --- src/pages/iou/request/step/IOURequestStepDate.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/request/step/IOURequestStepDate.tsx b/src/pages/iou/request/step/IOURequestStepDate.tsx index 26ea529cb108..2fea4c0d52e1 100644 --- a/src/pages/iou/request/step/IOURequestStepDate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDate.tsx @@ -159,6 +159,7 @@ const IOURequestStepDateWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, From 8a0b86d90763666faf9b0d3c2f62e5fba0e4a762 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 7 May 2024 12:46:08 -0600 Subject: [PATCH 157/219] update provision profile --- ios/NewApp_AdHoc.mobileprovision.gpg | Bin 11738 -> 11763 bytes ...c_Notification_Service.mobileprovision.gpg | Bin 11379 -> 11396 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 440309f63c6eb5230ccfadb316953fa0889b86fa..29d379151525b7ad3cbaaa740cc3641dacade482 100644 GIT binary patch literal 11763 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 VH$nb|jw0^-!Ng%uf{Wq%{QNLI^O67n literal 11738 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 diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index 2de81ee85018c1deb2671775c1f63738ef45feca..cf14d27d7d87a2cc2f6dd04fb4085a547172f96d 100644 GIT binary patch literal 11396 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 Date: Tue, 7 May 2024 20:32:56 +0100 Subject: [PATCH 158/219] Fix split amounts not displayed in scanned splits details page --- src/components/MoneyRequestConfirmationList.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index ffb996ea9d10..f38ac1a70d36 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -471,9 +471,8 @@ function MoneyRequestConfirmationList({ let amount: number | undefined = 0; if (iouAmount > 0) { amount = - isPolicyExpenseChat || !transaction?.comment?.splits - ? IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer) - : transaction.comment.splits.find((split) => split.accountID === participantOption.accountID)?.amount; + transaction?.comment?.splits?.find((split) => split.accountID === participantOption.accountID)?.amount ?? + IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); } return { ...participantOption, @@ -500,7 +499,7 @@ function MoneyRequestConfirmationList({ onAmountChange: (value: string) => onSplitShareChange(participantOption.accountID ?? 0, Number(value)), }, })); - }, [transaction, iouCurrencyCode, isPolicyExpenseChat, onSplitShareChange, payeePersonalDetails, selectedParticipants, currencyList, iouAmount, shouldShowReadOnlySplits, StyleUtils]); + }, [transaction, iouCurrencyCode, onSplitShareChange, payeePersonalDetails, selectedParticipants, currencyList, iouAmount, shouldShowReadOnlySplits, StyleUtils]); const isSplitModified = useMemo(() => { if (!transaction?.splitShares) { From 936bb9363caa83dd2bd7635743a90ec59dc0c7c2 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 7 May 2024 20:41:48 +0100 Subject: [PATCH 159/219] Cleanup --- src/components/TextInput/BaseTextInput/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 04d400424a87..c73509aa7b8f 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -296,7 +296,6 @@ function BaseTextInput( ]} > Date: Tue, 7 May 2024 21:04:33 +0000 Subject: [PATCH 160/219] Update version to 1.4.71-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f624c448bf82..429a294905f6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047100 - versionName "1.4.71-0" + versionCode 1001047101 + versionName "1.4.71-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 14bf7ffba924..6a8badfbb93b 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.71.0 + 1.4.71.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 6af8bb6ddc16..9b1ec50eb02e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.71.0 + 1.4.71.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index da2d70d0e859..fd47dce6ffd4 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.71 CFBundleVersion - 1.4.71.0 + 1.4.71.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index cdc8a6c1b04b..54ecfd05d377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.71-0", + "version": "1.4.71-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.71-0", + "version": "1.4.71-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1aabe53b5480..822a139473c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.71-0", + "version": "1.4.71-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 9fe4b1575bcaf073d7e8ba0e3d935f4f1a02cd43 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Tue, 7 May 2024 22:09:12 +0000 Subject: [PATCH 161/219] Update version to 1.4.71-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 429a294905f6..713268c07b1f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047101 - versionName "1.4.71-1" + versionCode 1001047102 + versionName "1.4.71-2" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 6a8badfbb93b..0f84b24d783b 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.71.1 + 1.4.71.2 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9b1ec50eb02e..e83366f32bad 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.71.1 + 1.4.71.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index fd47dce6ffd4..8fb4353bdfe2 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.71 CFBundleVersion - 1.4.71.1 + 1.4.71.2 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 54ecfd05d377..ba5a9e1a3c90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.71-1", + "version": "1.4.71-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.71-1", + "version": "1.4.71-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 822a139473c0..c31eab2cf5d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.71-1", + "version": "1.4.71-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 8f6aff12de287be1211bdf55104cc3104f065aa4 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 7 May 2024 15:49:59 -0700 Subject: [PATCH 162/219] Added vertical threshold offset check --- src/pages/home/report/ReportActionsList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 070d2229467b..9ef1a11aea79 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -503,7 +503,10 @@ function ReportActionsList({ const unreadMessageIndex = sortedVisibleReportActions.findIndex((action) => action.reportActionID === currentUnreadMarker); - if (unreadMessageIndex !== -1) { + // Checking that we have a valid unread message index and the user scroll + // offset is less than the threshold since we don't want to auto-scroll when + // the report is already open and New Messages marker is shown as user might be reading. + if (unreadMessageIndex !== -1 && scrollingVerticalOffset.current < VERTICAL_OFFSET_THRESHOLD) { // We're passing viewPosition: 1 to scroll to the top of the // unread message (marker) since we're using an inverted FlatList. reportScrollManager?.scrollToIndex(unreadMessageIndex, false, 1); From d009bd78c380b46cc5e6615793aeb557830dbb11 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 7 May 2024 13:13:43 -1000 Subject: [PATCH 163/219] Revert "Move Leave button into a row of the Report Details page" --- src/CONST.ts | 1 + src/components/ChatDetailsQuickActionsBar.tsx | 40 +++++++--- src/libs/PolicyUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/pages/ReportDetailsPage.tsx | 76 +++++-------------- 5 files changed, 49 insertions(+), 72 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 19720c05a93c..95cb4f94b169 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2055,6 +2055,7 @@ const CONST = { INFO: 'info', }, REPORT_DETAILS_MENU_ITEM: { + SHARE_CODE: 'shareCode', MEMBERS: 'member', INVITE: 'invite', SETTINGS: 'settings', diff --git a/src/components/ChatDetailsQuickActionsBar.tsx b/src/components/ChatDetailsQuickActionsBar.tsx index d289587ce953..f15fc31aec45 100644 --- a/src/components/ChatDetailsQuickActionsBar.tsx +++ b/src/components/ChatDetailsQuickActionsBar.tsx @@ -1,12 +1,11 @@ -import React from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; import * as Report from '@userActions/Report'; -import ROUTES from '@src/ROUTES'; import type {Report as OnyxReportType} from '@src/types/onyx'; import Button from './Button'; +import ConfirmModal from './ConfirmModal'; import * as Expensicons from './Icon/Expensicons'; type ChatDetailsQuickActionsBarProps = { @@ -15,26 +14,45 @@ type ChatDetailsQuickActionsBarProps = { function ChatDetailsQuickActionsBar({report}: ChatDetailsQuickActionsBarProps) { const styles = useThemeStyles(); + const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false); const {translate} = useLocalize(); const isPinned = !!report.isPinned; return ( + { + setIsLastMemberLeavingGroupModalVisible(false); + Report.leaveGroupChat(report.reportID); + }} + onCancel={() => setIsLastMemberLeavingGroupModalVisible(false)} + prompt={translate('groupChat.lastMemberWarning')} + confirmText={translate('common.leave')} + cancelText={translate('common.cancel')} + />