From 86aa3a106c36a27a766efe9ee24fcab7130e796d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Thu, 21 Sep 2023 10:37:22 +0200 Subject: [PATCH 001/244] remove unnecessary search actions --- src/pages/SearchPage.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index dd42ed80c3d4..39f75d414610 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import React, {useCallback, useEffect, useState, useMemo} from 'react'; +import React, {useCallback, useEffect, useState, useMemo, useRef} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; @@ -56,6 +56,7 @@ function SearchPage({betas, personalDetails, reports}) { }); const {translate} = useLocalize(); + const isMounted = useRef(false); const updateOptions = useCallback(() => { const { @@ -79,8 +80,13 @@ function SearchPage({betas, personalDetails, reports}) { }, []); useEffect(() => { + if (!isMounted.current) { + isMounted.current = true; + return; + } + debouncedUpdateOptions(); - }, [searchValue, debouncedUpdateOptions]); + }, [searchValue]); /** * Returns the sections needed for the OptionsSelector From afac89ec309687a8c562317de1327256aa0b710c Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Sat, 7 Oct 2023 01:04:23 +0700 Subject: [PATCH 002/244] fix: 28969 --- src/pages/home/ReportScreen.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 78f6edcf7dd3..faed80059507 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -157,6 +157,8 @@ function ReportScreen({ const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isBannerVisible, setIsBannerVisible] = useState(true); + const wasReportAccessibleRef = useRef(false); + const reportID = getReportID(route); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -179,6 +181,12 @@ function ReportScreen({ const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); + useEffect(() => { + if (report && report.reportID && !shouldHideReport) { + wasReportAccessibleRef.current = true; + } + }, [shouldHideReport, report]); + let headerView = ( (!firstRenderRef.current && !report.reportID && !isOptimisticDelete && !reportMetadata.isLoadingReportActions && !isLoading && !userLeavingStatus) || shouldHideReport, + () => (!wasReportAccessibleRef.current && !firstRenderRef.current && !report.reportID && !isOptimisticDelete && !reportMetadata.isLoadingReportActions && !isLoading && !userLeavingStatus) || shouldHideReport, [report, reportMetadata, isLoading, shouldHideReport, isOptimisticDelete, userLeavingStatus], ); From 3a20b60e93576a3b7b245a91e1ebfa997237e96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Thu, 26 Oct 2023 15:32:22 +0200 Subject: [PATCH 003/244] fixes after conflicts --- src/pages/SearchPage.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 3b572bcf0ebb..f9f3cc42f64a 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -18,8 +18,7 @@ import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; import Performance from '../libs/Performance'; import useLocalize from '../hooks/useLocalize'; -import networkPropTypes from '../components/networkPropTypes'; -import {withNetwork} from '../components/OnyxProvider'; +import useNetwork from "../hooks/useNetwork"; const propTypes = { /* Onyx Props */ @@ -33,9 +32,6 @@ const propTypes = { /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), - /** Network info */ - network: networkPropTypes, - /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, }; @@ -44,11 +40,10 @@ const defaultProps = { betas: [], personalDetails: {}, reports: {}, - network: {}, isSearchingForReports: false, }; -function SearchPage({betas, personalDetails, reports}) { +function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { // Data for initialization (runs only on the first render) const { recentReports: initialRecentReports, @@ -65,6 +60,7 @@ function SearchPage({betas, personalDetails, reports}) { userToInvite: initialUserToInvite, }); + const {isOffline} = useNetwork(); const {translate} = useLocalize(); const isMounted = useRef(false); @@ -185,12 +181,12 @@ function SearchPage({betas, personalDetails, reports}) { shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={ - props.network.isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '' + isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '' } onLayout={searchRendered} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus - isLoadingNewOptions={props.isSearchingForReports} + isLoadingNewOptions={isSearchingForReports} /> @@ -202,9 +198,7 @@ function SearchPage({betas, personalDetails, reports}) { SearchPage.propTypes = propTypes; SearchPage.defaultProps = defaultProps; SearchPage.displayName = 'SearchPage'; -export default compose( - withNetwork(), - withOnyx({ +export default withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, @@ -218,5 +212,4 @@ export default compose( key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, }, -}), -)(SearchPage); +})(SearchPage); From e212f254a80603a169a9d37910cb66b0ebe29d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Thu, 26 Oct 2023 15:42:10 +0200 Subject: [PATCH 004/244] fix lint error --- src/pages/SearchPage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index f9f3cc42f64a..d0da509197e1 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -92,6 +92,8 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { } debouncedUpdateOptions(); + // Ignoring the rule intentionally, we want to run the code only when search Value changes to prevent additional runs. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchValue]); /** From 0cd0edc8abf862c10c1f0f38d61a17552b47041a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Mon, 30 Oct 2023 18:04:34 +0100 Subject: [PATCH 005/244] fix lint error --- src/pages/SearchPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index fb325a5bc200..27148d97e9bb 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -17,8 +17,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; -import useLocalize from '../hooks/useLocalize'; -import useNetwork from "../hooks/useNetwork"; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from "@hooks/useNetwork"; const propTypes = { /* Onyx Props */ From 94e2deee606832478f5d28c22e4bc4226f7b8620 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 30 Oct 2023 20:45:04 +0200 Subject: [PATCH 006/244] ReportAction text for transactions edited in OldDot --- src/libs/ReportUtils.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 0e9091fd6cb2..d14336256894 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1844,6 +1844,8 @@ function getModifiedExpenseMessage(reportAction) { return Localize.translateLocal('iou.changedTheRequest'); } + const messageFragments = []; + const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && _.has(reportActionOriginalMessage, 'oldCurrency') && @@ -1864,12 +1866,12 @@ function getModifiedExpenseMessage(reportAction) { return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false)); } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true)); } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); @@ -1877,27 +1879,34 @@ function getModifiedExpenseMessage(reportAction) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false)); } if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true)); } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true)); } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true)); } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true)); } + + return messageFragments.reduce((acc, value, index) => { + if (index == 0) { + return acc + value; + } + return acc + ". " + value.charAt(0).toUpperCase() + value.slice(1); + }, ""); } /** From 6e5d6b04de5cfe67f3179ad65cb4d0d2340e3201 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 30 Oct 2023 21:02:22 +0200 Subject: [PATCH 007/244] Make Lint happy. --- src/libs/ReportUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d14336256894..3e8860543d5f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1902,10 +1902,10 @@ function getModifiedExpenseMessage(reportAction) { } return messageFragments.reduce((acc, value, index) => { - if (index == 0) { + if (index === 0) { return acc + value; } - return acc + ". " + value.charAt(0).toUpperCase() + value.slice(1); + return `${acc}. ${value.charAt(0).toUpperCase()}${value.slice(1)}`; }, ""); } From a7f3f383524cf5d3b1aef2a31006916aacb56e10 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 30 Oct 2023 21:07:35 +0200 Subject: [PATCH 008/244] Run prettier. --- src/libs/ReportUtils.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 3e8860543d5f..49bf6a5f6310 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1871,7 +1871,9 @@ function getModifiedExpenseMessage(reportAction) { const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true), + ); } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); @@ -1883,12 +1885,16 @@ function getModifiedExpenseMessage(reportAction) { } if (hasModifiedMerchant) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true), + ); } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true), + ); } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); @@ -1898,15 +1904,17 @@ function getModifiedExpenseMessage(reportAction) { const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true)); + messageFragments.push( + getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true), + ); } - + return messageFragments.reduce((acc, value, index) => { if (index === 0) { return acc + value; } return `${acc}. ${value.charAt(0).toUpperCase()}${value.slice(1)}`; - }, ""); + }, ''); } /** From f0b5986126073f4d8dab221dede715071774061a Mon Sep 17 00:00:00 2001 From: Luthfi Date: Wed, 1 Nov 2023 20:20:15 +0700 Subject: [PATCH 009/244] add form input styles checklist --- contributingGuides/REVIEWER_CHECKLIST.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index 16c8f88927b1..51908d696c1d 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -51,6 +51,9 @@ - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. - [ ] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account. +- [ ] If the PR modifies the form input styles: + - [ ] All the inputs inside a form should be aligned with each other + - [ ] Add the Design label so the Design team can review your changes - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. - [ ] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR. From 61da6b547ba5abe206d813b7631d593e397a4de8 Mon Sep 17 00:00:00 2001 From: Luthfi Date: Wed, 1 Nov 2023 20:27:59 +0700 Subject: [PATCH 010/244] Add the checklist to the PR template --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0396a7543b50..e516b8b3e0e1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -114,6 +114,9 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. - [ ] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account. +- [ ] If the PR modifies the form input styles: + - [ ] All the inputs inside a form should be aligned with each other + - [ ] Add the Design label so the Design team can review your changes - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. - [ ] I have checked off every checkbox in the PR author checklist, including those that don't apply to this PR. From 0b0359eb5982a94b53edfedcfd6c4273d94b4416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Thu, 2 Nov 2023 16:58:15 +0100 Subject: [PATCH 011/244] fix lint error --- src/pages/SearchPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 27148d97e9bb..70eaa0383a23 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -15,10 +15,10 @@ import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import personalDetailsPropType from './personalDetailsPropType'; -import reportPropTypes from './reportPropTypes'; import useLocalize from '@hooks/useLocalize'; import useNetwork from "@hooks/useNetwork"; +import personalDetailsPropType from './personalDetailsPropType'; +import reportPropTypes from './reportPropTypes'; const propTypes = { /* Onyx Props */ From 1df2c02b2f5495772d56f08def35b9ed998ab58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Tue, 7 Nov 2023 08:32:22 +0100 Subject: [PATCH 012/244] prettier --- src/pages/SearchPage.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 32d10ba539c8..3e6cb0333e3f 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useState, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; @@ -15,8 +17,6 @@ import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from "@hooks/useNetwork"; import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; @@ -187,9 +187,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { showTitleTooltip shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} - textInputAlert={ - isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '' - } + textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} onLayout={searchRendered} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus From a0a856cddc840617f1f5fd33416766b4b0d502f5 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:07:47 +0200 Subject: [PATCH 013/244] Improve message for MODIFIEDEXPENSE action --- src/languages/en.ts | 9 ++-- src/languages/es.ts | 9 ++-- src/libs/ReportUtils.js | 99 ++++++++++++++++++++++++++++++++--------- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d99b3c7d04d1..919548a3d918 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -573,11 +573,11 @@ export default { noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", changedTheRequest: 'changed the request', - setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `set the ${valueName} to ${newValueToDisplay}`, + setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `set the distance to ${newDistanceToDisplay}, which set the amount to ${newAmountToDisplay}`, - removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `removed the ${valueName} (previously ${oldValueToDisplay})`, + removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `changed the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, + `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, @@ -597,6 +597,9 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, enableWallet: 'Enable Wallet', + set: 'set', + changed: 'changed', + removed: 'removed', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index dea7760a35ce..8d5f678c63e8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -565,13 +565,13 @@ export default { noReimbursableExpenses: 'El importe de este informe no es válido', pendingConversionMessage: 'El total se actualizará cuando estés online', changedTheRequest: 'cambió la solicitud', - setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `estableció ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, + setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`, removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => - `eliminó ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, + `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `cambió ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, + `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, @@ -591,6 +591,9 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', + set: 'estableció', + changed: 'cambió', + removed: 'eliminó', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 1297171d3149..9f4b50965f3e 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1917,7 +1917,9 @@ function getModifiedExpenseMessage(reportAction) { return Localize.translateLocal('iou.changedTheRequest'); } - const messageFragments = []; + const removalFragments = []; + const setFragments = []; + const changeFragments = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -1939,14 +1941,26 @@ function getModifiedExpenseMessage(reportAction) { return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - messageFragments.push(getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false)); + const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + if (!oldAmount) { + setFragments.push(fragment); + } else if (!amount) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + if (!reportActionOriginalMessage.oldComment) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.newComment) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); @@ -1954,40 +1968,81 @@ function getModifiedExpenseMessage(reportAction) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false)); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + if (!formattedOldCreated) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.created) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } if (hasModifiedMerchant) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + if (!reportActionOriginalMessage.oldMerchant) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.merchant) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + if (!reportActionOriginalMessage.oldCategory) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.category) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - messageFragments.push(getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true)); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true); + if (!reportActionOriginalMessage.oldTag) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.tag) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - messageFragments.push( - getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true), - ); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + if (!reportActionOriginalMessage.oldBillable) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.billable) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } } - return messageFragments.reduce((acc, value, index) => { - if (index === 0) { - return acc + value; - } - return `${acc}. ${value.charAt(0).toUpperCase()}${value.slice(1)}`; - }, ''); + let message = ''; + if (setFragments.length > 0) { + message = setFragments.reduce((acc, value) => { + return `${acc} ${value},`; + }, `${message} ${Localize.translateLocal('iou.set')}`); + } + if (changeFragments.length > 0) { + message = changeFragments.reduce((acc, value) => { + return `${acc} ${value},`; + }, `${message} ${Localize.translateLocal('iou.changed')}`); + } + if (removalFragments.length > 0) { + message = removalFragments.reduce((acc, value) => { + return `${acc} ${value},`; + }, `${message} ${Localize.translateLocal('iou.removed')}`); + } + message = `${message.substring(1, message.length - 1)}.`; + return message; } /** From 960d00a36d28f7f60966df3686d6d2be6f87db68 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:09:28 +0200 Subject: [PATCH 014/244] Add a safety check --- src/libs/ReportUtils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 9f4b50965f3e..97bc468de8eb 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2041,6 +2041,9 @@ function getModifiedExpenseMessage(reportAction) { return `${acc} ${value},`; }, `${message} ${Localize.translateLocal('iou.removed')}`); } + if (message === '') { + return message; + } message = `${message.substring(1, message.length - 1)}.`; return message; } From a4e82a8baaf2eabf78a40848dbaa28179ad96d89 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:21:09 +0200 Subject: [PATCH 015/244] Improve MODIFIEDEXPENSE action message --- src/libs/ReportUtils.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 97bc468de8eb..a2de3dc73161 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2026,20 +2026,20 @@ function getModifiedExpenseMessage(reportAction) { } let message = ''; - if (setFragments.length > 0) { - message = setFragments.reduce((acc, value) => { + if (changeFragments.length > 0) { + message = changeFragments.reduce((acc, value, index) => { return `${acc} ${value},`; - }, `${message} ${Localize.translateLocal('iou.set')}`); + }, `${message}\n${Localize.translateLocal('iou.changed')}`); } - if (changeFragments.length > 0) { - message = changeFragments.reduce((acc, value) => { + if (setFragments.length > 0) { + message = setFragments.reduce((acc, value) => { return `${acc} ${value},`; - }, `${message} ${Localize.translateLocal('iou.changed')}`); + }, `${message}\n${Localize.translateLocal('iou.set')}`); } if (removalFragments.length > 0) { message = removalFragments.reduce((acc, value) => { return `${acc} ${value},`; - }, `${message} ${Localize.translateLocal('iou.removed')}`); + }, `${message}\n${Localize.translateLocal('iou.removed')}`); } if (message === '') { return message; From b9b7ef462310e69498e261137bc8ab619af75c98 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:33:22 +0200 Subject: [PATCH 016/244] Improve MODIFIEDEXPENSE action message --- src/libs/ReportUtils.js | 43 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index a2de3dc73161..5680ea7b017a 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2028,17 +2028,50 @@ function getModifiedExpenseMessage(reportAction) { let message = ''; if (changeFragments.length > 0) { message = changeFragments.reduce((acc, value, index) => { - return `${acc} ${value},`; + if (index === changeFragments.length - 1) { + if (changeFragments.length === 1) { + return `${acc} ${value}.`; + } else if (changeFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; }, `${message}\n${Localize.translateLocal('iou.changed')}`); } if (setFragments.length > 0) { - message = setFragments.reduce((acc, value) => { - return `${acc} ${value},`; + message = setFragments.reduce((acc, value, index) => { + if (index === setFragments.length - 1) { + if (setFragments.length === 1) { + return `${acc} ${value}.`; + } else if (setFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; }, `${message}\n${Localize.translateLocal('iou.set')}`); } if (removalFragments.length > 0) { - message = removalFragments.reduce((acc, value) => { - return `${acc} ${value},`; + message = removalFragments.reduce((acc, value, index) => { + if (index === removalFragments.length - 1) { + if (removalFragments.length === 1) { + return `${acc} ${value}.`; + } else if (removalFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; }, `${message}\n${Localize.translateLocal('iou.removed')}`); } if (message === '') { From 5190539d1b65224687b503204749623915271deb Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 00:39:25 +0200 Subject: [PATCH 017/244] Remove redundancy. --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5680ea7b017a..5004e5d87dc0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2077,7 +2077,7 @@ function getModifiedExpenseMessage(reportAction) { if (message === '') { return message; } - message = `${message.substring(1, message.length - 1)}.`; + message = `${message.substring(1, message.length)}`; return message; } From b5518be159aa4cea4aad09ee70ddb760e5b42245 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 15:59:14 +0200 Subject: [PATCH 018/244] Update function doc --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5004e5d87dc0..d5f56bea8c5c 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1857,7 +1857,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip } /** - * Get the proper message schema for modified expense message. + * Get the proper message schema for a modified field on the expense. * * @param {String} newValue * @param {String} oldValue From be978f9b0138b1474094ae1a3d95ea35eefea982 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 16:17:08 +0200 Subject: [PATCH 019/244] Create reusable function --- src/libs/ReportUtils.js | 84 +++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d5f56bea8c5c..13ffeb6432f5 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1857,7 +1857,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip } /** - * Get the proper message schema for a modified field on the expense. + * Get the proper message schema for a modified a field on the expense. * * @param {String} newValue * @param {String} oldValue @@ -1880,6 +1880,36 @@ function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); } +/** + * Get the proper message line for a modified expense. + * + * @param {String} newValue + * @param {String} oldValue + * @param {String} valueName + * @param {Boolean} valueInQuotes + * @returns {String} + */ + +function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { + if (messageFragments.length === 0) { + return ''; + } + return messageFragments.reduce((acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; + } else if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } else { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + } else if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; + }, prefix); +} + /** * Get the proper message schema for modified distance message. * @@ -2025,55 +2055,9 @@ function getModifiedExpenseMessage(reportAction) { } } - let message = ''; - if (changeFragments.length > 0) { - message = changeFragments.reduce((acc, value, index) => { - if (index === changeFragments.length - 1) { - if (changeFragments.length === 1) { - return `${acc} ${value}.`; - } else if (changeFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - } else if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, `${message}\n${Localize.translateLocal('iou.changed')}`); - } - if (setFragments.length > 0) { - message = setFragments.reduce((acc, value, index) => { - if (index === setFragments.length - 1) { - if (setFragments.length === 1) { - return `${acc} ${value}.`; - } else if (setFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - } else if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, `${message}\n${Localize.translateLocal('iou.set')}`); - } - if (removalFragments.length > 0) { - message = removalFragments.reduce((acc, value, index) => { - if (index === removalFragments.length - 1) { - if (removalFragments.length === 1) { - return `${acc} ${value}.`; - } else if (removalFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - } else if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, `${message}\n${Localize.translateLocal('iou.removed')}`); - } + let message = getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { return message; } From b31ba92326163585034ac04cd466d39765b7876b Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 16:18:59 +0200 Subject: [PATCH 020/244] Run prettier --- src/languages/en.ts | 3 +-- src/languages/es.ts | 3 +-- src/libs/ReportUtils.js | 51 +++++++++++++++++++++++++++++------------ 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 919548a3d918..3be57af83cc5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -576,8 +576,7 @@ export default { setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `set the distance to ${newDistanceToDisplay}, which set the amount to ${newAmountToDisplay}`, removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, - updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, + updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 8d5f678c63e8..e2eb899a343a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -568,8 +568,7 @@ export default { setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`, - removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => - `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, + removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 13ffeb6432f5..68555644405c 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1897,7 +1897,7 @@ function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { return messageFragments.reduce((acc, value, index) => { if (index === messageFragments.length - 1) { if (messageFragments.length === 1) { - return `${acc} ${value}.`; + return `${acc} ${value}.`; } else if (messageFragments.length === 2) { return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; } else { @@ -1976,19 +1976,24 @@ function getModifiedExpenseMessage(reportAction) { setFragments.push(fragment); } else if (!amount) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.newComment, + reportActionOriginalMessage.oldComment, + Localize.translateLocal('common.description'), + true, + ); if (!reportActionOriginalMessage.oldComment) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.newComment) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } @@ -2003,30 +2008,40 @@ function getModifiedExpenseMessage(reportAction) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.created) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } if (hasModifiedMerchant) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.merchant, + reportActionOriginalMessage.oldMerchant, + Localize.translateLocal('common.merchant'), + true, + ); if (!reportActionOriginalMessage.oldMerchant) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.merchant) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.category, + reportActionOriginalMessage.oldCategory, + Localize.translateLocal('common.category'), + true, + ); if (!reportActionOriginalMessage.oldCategory) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.category) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } @@ -2038,26 +2053,32 @@ function getModifiedExpenseMessage(reportAction) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.tag) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.billable, + reportActionOriginalMessage.oldBillable, + Localize.translateLocal('iou.request'), + true, + ); if (!reportActionOriginalMessage.oldBillable) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.billable) { removalFragments.push(fragment); - } else { + } else { changeFragments.push(fragment); } } - let message = getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); + let message = + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { return message; } From 376944c425bc61756f7421bd47ed26f5794b90c8 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 16:22:23 +0200 Subject: [PATCH 021/244] Fix lint. --- src/libs/ReportUtils.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 68555644405c..70487dc0862f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1898,12 +1898,13 @@ function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { if (index === messageFragments.length - 1) { if (messageFragments.length === 1) { return `${acc} ${value}.`; - } else if (messageFragments.length === 2) { + } + if (messageFragments.length === 2) { return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } else { - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; } - } else if (index === 0) { + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + if (index === 0) { return `${acc} ${value}`; } return `${acc}, ${value}`; From b9f42439e8ff3ee28402bb3b91f9530445c41cbf Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 8 Nov 2023 18:11:51 +0200 Subject: [PATCH 022/244] Update src/libs/ReportUtils.js Co-authored-by: Tim Golen --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 70487dc0862f..07265d95edc7 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1857,7 +1857,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip } /** - * Get the proper message schema for a modified a field on the expense. + * Get the proper message schema for a modified field on the expense. * * @param {String} newValue * @param {String} oldValue From 271d86cb408b7f949d7bd6cfaea5c8aef5312e8f Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 9 Nov 2023 17:29:32 +0100 Subject: [PATCH 023/244] Props types for MenuItem --- src/components/{MenuItem.js => MenuItem.tsx} | 162 ++++++++++++++++++- 1 file changed, 157 insertions(+), 5 deletions(-) rename src/components/{MenuItem.js => MenuItem.tsx} (79%) diff --git a/src/components/MenuItem.js b/src/components/MenuItem.tsx similarity index 79% rename from src/components/MenuItem.js rename to src/components/MenuItem.tsx index 103d063f9024..3b6ad37a9f5e 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.tsx @@ -1,6 +1,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {useEffect, useMemo} from 'react'; -import {View} from 'react-native'; +import React, {ForwardedRef, ReactNode, useEffect, useMemo} from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; @@ -13,6 +13,8 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; +import AvatarType from '@src/types/onyx/Avatar'; +import { AnimatedStyle } from 'react-native-reanimated'; import Avatar from './Avatar'; import Badge from './Badge'; import DisplayNames from './DisplayNames'; @@ -20,14 +22,164 @@ import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import * as defaultWorkspaceAvatars from './Icon/WorkspaceDefaultAvatars'; -import menuItemPropTypes from './menuItemPropTypes'; import MultipleAvatars from './MultipleAvatars'; import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction'; import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -const propTypes = menuItemPropTypes; +type MenuItemProps = { + /** Text to be shown as badge near the right end. */ + badgeText: string; + + /** Function to fire when component is pressed */ + onPress?: (event: Event) => void; + + /** Used to apply offline styles to child text components */ + style?: StyleProp; + + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; + + /** Used to apply styles specifically to the title */ + titleStyle?: StyleProp; + + /** Icon to display on the left side of component */ + icon: ReactNode | string | AvatarType; + + /** Secondary icon to display on the left side of component, right of the icon */ + secondaryIcon: ReactNode; + + /** Icon Width */ + iconWidth: number; + + /** Icon Height */ + iconHeight: number; + + /** Text to display for the item */ + title: string; + + /** Text that appears above the title */ + label: string; + + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon: boolean; + + /** Icon to display at right side of title */ + titleIcon: () => void; + + /** Boolean whether to display the right icon */ + shouldShowRightIcon: boolean; + + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState: boolean; + + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle: boolean; + + /** Should the description be shown above the title (instead of the other way around) */ + shouldShowDescriptionOnTop: boolean; + + /** Whether this item is selected */ + isSelected: boolean; + + /** A boolean flag that gives the icon a green fill if true */ + success: boolean; + + /** Overrides the icon for shouldShowRightIcon */ + iconRight: ReactNode; + + /** A description text to show under the title */ + description: string; + + /** Any additional styles to pass to the icon container. */ + iconStyles: Array>; + + /** The fill color to pass into the icon. */ + iconFill: string; + + /** The fill color to pass into the secondary icon. */ + secondaryIconFill: string; + + /** Whether item is focused or active */ + focused: boolean; + + /** Should we disable this menu item? */ + disabled: boolean; + + /** A right-aligned subtitle for this menu option */ + subtitle: string | number; + + /** Flag to choose between avatar image or an icon */ + iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; + + /** Whether the menu item should be interactive at all */ + interactive: boolean; + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon: string | (() => void); + + /** Avatars to show on the right of the menu item */ + floatRightAvatars: AvatarType; + + /** The type of brick road indicator to show. */ + brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally: boolean; + + /** Prop to represent the size of the float right avatar images to be shown */ + floatRightAvatarSize: typeof CONST.AVATAR_SIZE; + + /** Prop to represent the size of the avatar images to be shown */ + avatarSize: typeof CONST.AVATAR_SIZE; + + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction: () => void; + + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection: boolean; + + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle: Array>>, + + /** Text to display under the main item */ + furtherDetails: string; + + /** An icon to display under the main item */ + furtherDetailsIcon: ReactNode | string; + + /** The action accept for anonymous user or not */ + isAnonymousAction: boolean; + + /** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu: boolean; + + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled: boolean; + + /** Error to display below the title */ + error: string; + + /** Should render the content in HTML format */ + shouldRenderAsHTML: boolean; + + /** Component to be displayed on the right */ + rightComponent: ReactNode; + + /** Should render component on the right */ + shouldShowRightComponent: boolean; + + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips: ReactNode[]; + + /** Should check anonymous user in onPress function */ + shouldCheckActionAllowedOnPress: boolean; +}; + +// TODO: Destructure props +// TODO: Adjust default values +// TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { badgeText: undefined, @@ -84,7 +236,7 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -const MenuItem = React.forwardRef((props, ref) => { +const MenuItem = React.forwardRef((props: MenuItemProps, ref: ForwardedRef) => { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); From 8dba84c7e4ca8d0f1e090fd2a7a269d9ece5d6ac Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 9 Nov 2023 17:42:07 +0100 Subject: [PATCH 024/244] Destructure props --- src/components/MenuItem.tsx | 222 ++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 110 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 3b6ad37a9f5e..590bdf73cce8 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,6 +1,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import React, {ForwardedRef, ReactNode, useEffect, useMemo} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; +import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; @@ -236,234 +236,236 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -const MenuItem = React.forwardRef((props: MenuItemProps, ref: ForwardedRef) => { +const MenuItem = React.forwardRef(({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + shouldCheckActionAllowedOnPress +}: MenuItemProps, ref: ForwardedRef) => { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); - const isDeleted = _.contains(props.style, styles.offlineFeedback.deleted); - const descriptionVerticalMargin = props.shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; + const isDeleted = _.contains(style, styles.offlineFeedback.deleted); + const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; const titleTextStyle = StyleUtils.combineStyles( [ styles.flexShrink1, styles.popoverMenuText, - props.icon && !_.isArray(props.icon) && (props.avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), - props.shouldShowBasicTitle ? undefined : styles.textStrong, - props.shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, - props.numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, - props.interactive && props.disabled ? {...styles.userSelectNone} : undefined, + icon && !_.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), + shouldShowBasicTitle ? undefined : styles.textStrong, + shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, + numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, + interactive && disabled ? {...styles.userSelectNone} : undefined, styles.ltr, isDeleted ? styles.offlineFeedback.deleted : undefined, ], - props.titleStyle, + titleStyle, ); const descriptionTextStyle = StyleUtils.combineStyles([ styles.textLabelSupporting, - props.icon && !_.isArray(props.icon) ? styles.ml3 : undefined, - props.title ? descriptionVerticalMargin : StyleUtils.getFontSizeStyle(variables.fontSizeNormal), - props.descriptionTextStyle, + icon && !_.isArray(icon) ? styles.ml3 : undefined, + title ? descriptionVerticalMargin : StyleUtils.getFontSizeStyle(variables.fontSizeNormal), + descriptionTextStyle, isDeleted ? styles.offlineFeedback.deleted : undefined, ]); - const fallbackAvatarSize = props.viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; + const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; const titleRef = React.useRef(''); useEffect(() => { - if (!props.title || (titleRef.current.length && titleRef.current === props.title) || !props.shouldParseTitle) { + if (!title || (titleRef.current.length && titleRef.current === title) || !shouldParseTitle) { return; } const parser = new ExpensiMark(); - setHtml(parser.replace(props.title)); - titleRef.current = props.title; - }, [props.title, props.shouldParseTitle]); + setHtml(parser.replace(title)); + titleRef.current = title; + }, [title, shouldParseTitle]); const getProcessedTitle = useMemo(() => { let title = ''; - if (props.shouldRenderAsHTML) { - title = convertToLTR(props.title); + if (shouldRenderAsHTML) { + title = convertToLTR(title); } - if (props.shouldParseTitle) { + if (shouldParseTitle) { title = html; } return title ? `${title}` : ''; - }, [props.title, props.shouldRenderAsHTML, props.shouldParseTitle, html]); + }, [title, shouldRenderAsHTML, shouldParseTitle, html]); - const hasPressableRightComponent = props.iconRight || (props.rightComponent && props.shouldShowRightComponent); + const hasPressableRightComponent = iconRight || (rightComponent && shouldShowRightComponent); const renderTitleContent = () => { - if (props.titleWithTooltips && _.isArray(props.titleWithTooltips) && props.titleWithTooltips.length > 0) { + if (titleWithTooltips && _.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { return ( ); } - return convertToLTR(props.title); + return convertToLTR(title); }; - const onPressAction = (e) => { - if (props.disabled || !props.interactive) { + const onPressAction = (event: GestureResponderEvent | KeyboardEvent) => { + if (disabled || !interactive) { return; } - if (e && e.type === 'click') { - e.currentTarget.blur(); + if (event && event.type === 'click') { + event.currentTarget.blur(); } - props.onPress(e); + onPress(e); }; return ( {(isHovered) => ( props.shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPress={shouldCheckActionAllowedOnPress ? Session.checkIfActionIsAllowed(onPressAction, isAnonymousAction) : onPressAction} + onPressIn={() => shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} - onSecondaryInteraction={props.onSecondaryInteraction} + onSecondaryInteraction={onSecondaryInteraction} style={({pressed}) => [ - props.style, - !props.interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), - (isHovered || pressed) && props.hoverAndPressStyle, - ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), - props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, + style, + !interactive && styles.cursorDefault, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true), + (isHovered || pressed) && hoverAndPressStyle, + ...(_.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), + shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, ]} - disabled={props.disabled} + disabled={disabled} ref={ref} role={CONST.ACCESSIBILITY_ROLE.MENUITEM} - accessibilityLabel={props.title ? props.title.toString() : ''} + accessibilityLabel={title ? title.toString() : ''} > {({pressed}) => ( <> - {Boolean(props.label) && ( - + {Boolean(label) && ( + - {props.label} + {label} )} - - {Boolean(props.icon) && _.isArray(props.icon) && ( + + {Boolean(icon) && _.isArray(icon) && ( )} - {Boolean(props.icon) && !_.isArray(props.icon) && ( - - {props.iconType === CONST.ICON_TYPE_ICON && ( + {Boolean(icon) && !_.isArray(icon) && ( + + {iconType === CONST.ICON_TYPE_ICON && ( )} - {props.iconType === CONST.ICON_TYPE_WORKSPACE && ( + {iconType === CONST.ICON_TYPE_WORKSPACE && ( )} - {props.iconType === CONST.ICON_TYPE_AVATAR && ( + {iconType === CONST.ICON_TYPE_AVATAR && ( )} )} - {Boolean(props.secondaryIcon) && ( - + {Boolean(secondaryIcon) && ( + )} - - {Boolean(props.description) && props.shouldShowDescriptionOnTop && ( + + {Boolean(description) && shouldShowDescriptionOnTop && ( - {props.description} + {description} )} - {Boolean(props.title) && (Boolean(props.shouldRenderAsHTML) || (Boolean(props.shouldParseTitle) && Boolean(html.length))) && ( + {Boolean(title) && (Boolean(shouldRenderAsHTML) || (Boolean(shouldParseTitle) && Boolean(html.length))) && ( )} - {!props.shouldRenderAsHTML && !props.shouldParseTitle && Boolean(props.title) && ( + {!shouldRenderAsHTML && !shouldParseTitle && Boolean(title) && ( {renderTitleContent()} )} - {Boolean(props.shouldShowTitleIcon) && ( + {Boolean(shouldShowTitleIcon) && ( )} - {Boolean(props.description) && !props.shouldShowDescriptionOnTop && ( + {Boolean(description) && !shouldShowDescriptionOnTop && ( - {props.description} + {description} )} - {Boolean(props.error) && ( + {Boolean(error) && ( - {props.error} + {error} )} - {Boolean(props.furtherDetails) && ( + {Boolean(furtherDetails) && ( style={[styles.furtherDetailsText, styles.ph2, styles.pt1]} numberOfLines={2} > - {props.furtherDetails} + {furtherDetails} )} @@ -480,52 +482,52 @@ const MenuItem = React.forwardRef((props: MenuItemProps, ref: ForwardedRef - {Boolean(props.badgeText) && ( + {Boolean(badgeText) && ( )} {/* Since subtitle can be of type number, we should allow 0 to be shown */} - {(props.subtitle || props.subtitle === 0) && ( + {(subtitle || subtitle === 0) && ( - {props.subtitle} + {subtitle} )} - {!_.isEmpty(props.floatRightAvatars) && ( - + {!_.isEmpty(floatRightAvatars) && ( + )} - {Boolean(props.brickRoadIndicator) && ( + {Boolean(brickRoadIndicator) && ( )} - {Boolean(props.shouldShowRightIcon) && ( - + {Boolean(shouldShowRightIcon) && ( + )} - {props.shouldShowRightComponent && props.rightComponent} - {props.shouldShowSelectedState && } + {shouldShowRightComponent && rightComponent} + {shouldShowSelectedState && } )} From 12ec1ed805e7c4afa23b4cb683d382989e6f30df Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 9 Nov 2023 17:44:30 +0100 Subject: [PATCH 025/244] Make the component a function declaration --- src/components/MenuItem.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 590bdf73cce8..3036269790e8 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,5 +1,5 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {ForwardedRef, ReactNode, useEffect, useMemo} from 'react'; +import React, {ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -236,9 +236,9 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -const MenuItem = React.forwardRef(({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, +function MenuItem({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress -}: MenuItemProps, ref: ForwardedRef) => { +}: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); @@ -535,10 +535,8 @@ const MenuItem = React.forwardRef(({badgeText,onPress,style,wrapperStyle,titleSt )} ); -}); +} -MenuItem.propTypes = propTypes; -MenuItem.defaultProps = defaultProps; MenuItem.displayName = 'MenuItem'; -export default MenuItem; +export default forwardRef(MenuItem); From 49d35b131c9e8ad9e7593da7cf01f4be04ef0d8c Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 11:28:42 +0100 Subject: [PATCH 026/244] Type onPress --- src/components/MenuItem.tsx | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 3036269790e8..b6de792d4a0d 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -28,12 +28,23 @@ import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -type MenuItemProps = { - /** Text to be shown as badge near the right end. */ - badgeText: string; - +type ResponsiveProps = { /** Function to fire when component is pressed */ - onPress?: (event: Event) => void; + onPress: (event: GestureResponderEvent | KeyboardEvent) => void; + + interactive?: true; +} + +type UnresponsiveProps = { + onPress?: undefined; + + /** Whether the menu item should be interactive at all */ + interactive: false; +} + +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { + /** Text to be shown as badge near the right end. */ + badgeText?: string; /** Used to apply offline styles to child text components */ style?: StyleProp; @@ -113,9 +124,6 @@ type MenuItemProps = { /** Flag to choose between avatar image or an icon */ iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; - /** Whether the menu item should be interactive at all */ - interactive: boolean; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon: string | (() => void); @@ -182,7 +190,6 @@ type MenuItemProps = { // TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { - badgeText: undefined, shouldShowRightIcon: false, shouldShowSelectedState: false, shouldShowBasicTitle: false, @@ -236,7 +243,9 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -function MenuItem({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, +function MenuItem({badgeText, onPress, + // Props not validated below - Validate if required and default value + ,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -313,11 +322,11 @@ function MenuItem({badgeText,onPress,style,wrapperStyle,titleStyle,icon,secondar return; } - if (event && event.type === 'click') { - event.currentTarget.blur(); + if (event && event.type === 'click' && event.currentTarget instanceof EventTarget) { + (event.currentTarget as HTMLElement).blur(); } - onPress(e); + onPress(event); }; return ( From 0cedba9095e705a99c5f50fc89d81dc553f96d8d Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 11:31:56 +0100 Subject: [PATCH 027/244] Validate style props --- src/components/MenuItem.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index b6de792d4a0d..84e19fcbd5ad 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -55,6 +55,8 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Used to apply styles specifically to the title */ titleStyle?: StyleProp; + // ------------------------------- VALID PROPS ABOVE + /** Icon to display on the left side of component */ icon: ReactNode | string | AvatarType; @@ -196,9 +198,6 @@ const defaultProps = { shouldShowDescriptionOnTop: false, shouldShowHeaderTitle: false, shouldParseTitle: false, - wrapperStyle: [], - style: styles.popoverMenuItem, - titleStyle: {}, shouldShowTitleIcon: false, titleIcon: () => {}, descriptionTextStyle: styles.breakWord, @@ -217,7 +216,6 @@ const defaultProps = { isSelected: false, subtitle: undefined, iconType: CONST.ICON_TYPE_ICON, - onPress: () => {}, onSecondaryInteraction: undefined, interactive: true, fallbackIcon: Expensicons.FallbackAvatar, @@ -243,9 +241,9 @@ const defaultProps = { shouldCheckActionAllowedOnPress: true, }; -function MenuItem({badgeText, onPress, +function MenuItem({badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, // Props not validated below - Validate if required and default value - ,style,wrapperStyle,titleStyle,icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); From b6e092535411ae4f746cd3c394ad26ebdbf81f26 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 16:17:08 +0100 Subject: [PATCH 028/244] Prop types -> Typescript props --- src/components/MenuItem.tsx | 185 ++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 95 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 84e19fcbd5ad..8c9ab27ce3b5 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,5 +1,5 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; +import React, {FC, ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -15,6 +15,8 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import AvatarType from '@src/types/onyx/Avatar'; import { AnimatedStyle } from 'react-native-reanimated'; +import IconType from '@types/Icon'; +import { SvgProps } from 'react-native-svg'; import Avatar from './Avatar'; import Badge from './Badge'; import DisplayNames from './DisplayNames'; @@ -42,7 +44,35 @@ type UnresponsiveProps = { interactive: false; } -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { +type TitleIconProps = { + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon: true; + + /** Icon to display at right side of title */ + titleIcon: IconType; +} + +type NoTitleIconProps = { + shouldShowTitleIcon?: false; + + titleIcon?: undefined; +} + +type RightIconProps = { + /** Boolean whether to display the right icon */ + shouldShowRightIcon: true; + + /** Overrides the icon for shouldShowRightIcon */ + iconRight: IconType; +} + +type NoRightIconProps = { + shouldShowRightIcon?: false; + + iconRight?: IconType; +} + +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & (RightIconProps | NoRightIconProps) &{ /** Text to be shown as badge near the right end. */ badgeText?: string; @@ -55,79 +85,79 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Used to apply styles specifically to the title */ titleStyle?: StyleProp; - // ------------------------------- VALID PROPS ABOVE + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle: StyleProp>; /** Icon to display on the left side of component */ - icon: ReactNode | string | AvatarType; + icon?: ReactNode | string | AvatarType; + + /** The fill color to pass into the icon. */ + iconFill?: string; /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon: ReactNode; + secondaryIcon?: ReactNode; + + /** The fill color to pass into the secondary icon. */ + secondaryIconFill?: string; + + /** Flag to choose between avatar image or an icon */ + iconType?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; /** Icon Width */ - iconWidth: number; + iconWidth?: number; /** Icon Height */ - iconHeight: number; + iconHeight?: number; - /** Text to display for the item */ - title: string; + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; - /** Text that appears above the title */ - label: string; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: FC; - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon: boolean; + /** An icon to display under the main item */ + furtherDetailsIcon?: IconType; - /** Icon to display at right side of title */ - titleIcon: () => void; + /** A description text to show under the title */ + description?: string; - /** Boolean whether to display the right icon */ - shouldShowRightIcon: boolean; - - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState: boolean; - - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle: boolean; - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop: boolean; - - /** Whether this item is selected */ - isSelected: boolean; - + shouldShowDescriptionOnTop?: boolean; + + /** Error to display below the title */ + error?: string; + /** A boolean flag that gives the icon a green fill if true */ - success: boolean; + success?: boolean; - /** Overrides the icon for shouldShowRightIcon */ - iconRight: ReactNode; + /** Whether item is focused or active */ + focused?: boolean; - /** A description text to show under the title */ - description: string; + /** Should we disable this menu item? */ + disabled?: boolean; - /** Any additional styles to pass to the icon container. */ - iconStyles: Array>; + /** Text that appears above the title */ + label?: string; - /** The fill color to pass into the icon. */ - iconFill: string; + /** Text to display for the item */ + title?: string; - /** The fill color to pass into the secondary icon. */ - secondaryIconFill: string; + /** A right-aligned subtitle for this menu option */ + subtitle?: string | number; - /** Whether item is focused or active */ - focused: boolean; + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState?: boolean; - /** Should we disable this menu item? */ - disabled: boolean; + /** Whether this item is selected */ + isSelected?: boolean; - /** A right-aligned subtitle for this menu option */ - subtitle: string | number; + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally: boolean; - /** Flag to choose between avatar image or an icon */ - iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_WORKSPACE; + // ------------------------------- VALID PROPS ABOVE - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: string | (() => void); + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle: boolean; /** Avatars to show on the right of the menu item */ floatRightAvatars: AvatarType; @@ -135,9 +165,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** The type of brick road indicator to show. */ brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; - /** Prop to represent the size of the float right avatar images to be shown */ floatRightAvatarSize: typeof CONST.AVATAR_SIZE; @@ -150,15 +177,9 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ shouldBlockSelection: boolean; - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: Array>>, - /** Text to display under the main item */ furtherDetails: string; - /** An icon to display under the main item */ - furtherDetailsIcon: ReactNode | string; - /** The action accept for anonymous user or not */ isAnonymousAction: boolean; @@ -168,9 +189,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Should we grey out the menu item when it is disabled? */ shouldGreyOutWhenDisabled: boolean; - /** Error to display below the title */ - error: string; - /** Should render the content in HTML format */ shouldRenderAsHTML: boolean; @@ -192,58 +210,35 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { // TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { - shouldShowRightIcon: false, - shouldShowSelectedState: false, shouldShowBasicTitle: false, - shouldShowDescriptionOnTop: false, shouldShowHeaderTitle: false, shouldParseTitle: false, - shouldShowTitleIcon: false, - titleIcon: () => {}, descriptionTextStyle: styles.breakWord, - success: false, - icon: undefined, - secondaryIcon: undefined, - iconWidth: undefined, - iconHeight: undefined, - description: undefined, - iconRight: Expensicons.ArrowRight, - iconStyles: [], - iconFill: undefined, - secondaryIconFill: undefined, - focused: false, - disabled: false, - isSelected: false, - subtitle: undefined, - iconType: CONST.ICON_TYPE_ICON, - onSecondaryInteraction: undefined, interactive: true, - fallbackIcon: Expensicons.FallbackAvatar, brickRoadIndicator: '', floatRightAvatars: [], - shouldStackHorizontally: false, avatarSize: CONST.AVATAR_SIZE.DEFAULT, - floatRightAvatarSize: undefined, shouldBlockSelection: false, - hoverAndPressStyle: [], furtherDetails: '', - furtherDetailsIcon: undefined, isAnonymousAction: false, isSmallAvatarSubscriptMenu: false, - title: '', numberOfLinesTitle: 1, shouldGreyOutWhenDisabled: true, - error: '', shouldRenderAsHTML: false, - rightComponent: undefined, shouldShowRightComponent: false, titleWithTooltips: [], shouldCheckActionAllowedOnPress: true, }; -function MenuItem({badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, +function MenuItem({ + badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, + icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, + shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, + description, error, success = false, focused = false, disabled = false, + title, subtitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, + shouldShowDescriptionOnTop = false, // Props not validated below - Validate if required and default value - icon,secondaryIcon,iconWidth,iconHeight,title,label,shouldShowTitleIcon,titleIcon,shouldShowRightIcon,shouldShowSelectedState,shouldShowBasicTitle,shouldShowDescriptionOnTop,isSelected,success,iconRight,description,iconStyles,iconFill,secondaryIconFill,focused,disabled,subtitle,iconType,interactive,fallbackIcon,floatRightAvatars,brickRoadIndicator = '',shouldStackHorizontally,floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,hoverAndPressStyle,furtherDetails,furtherDetailsIcon,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,error,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + shouldShowBasicTitle,interactive,floatRightAvatars,brickRoadIndicator = '',floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -373,7 +368,7 @@ function MenuItem({badgeText, onPress, style = styles.popoverMenuItem, wrapperSt /> )} {Boolean(icon) && !_.isArray(icon) && ( - + {iconType === CONST.ICON_TYPE_ICON && ( )} {Boolean(secondaryIcon) && ( - + Date: Fri, 10 Nov 2023 16:31:20 +0100 Subject: [PATCH 029/244] Prop types -> Typescript props --- src/components/MenuItem.tsx | 63 ++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8c9ab27ce3b5..8c7e426a4cfe 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -72,7 +72,22 @@ type NoRightIconProps = { iconRight?: IconType; } -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & (RightIconProps | NoRightIconProps) &{ +type RightComponent = { + /** Should render component on the right */ + shouldShowRightComponent: true; + + /** Component to be displayed on the right */ + rightComponent: ReactNode; +} + +type NoRightComponent = { + shouldShowRightComponent?: false; + + rightComponent?: undefined; +} + +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & +(RightIconProps | NoRightIconProps) & (RightComponent | NoRightComponent) & { /** Text to be shown as badge near the right end. */ badgeText?: string; @@ -145,6 +160,9 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** A right-aligned subtitle for this menu option */ subtitle?: string | number; + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle?: boolean; + /** Should we make this selectable with a checkbox */ shouldShowSelectedState?: boolean; @@ -154,23 +172,23 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** Prop to identify if we should load avatars vertically instead of diagonally */ shouldStackHorizontally: boolean; - // ------------------------------- VALID PROPS ABOVE + /** Prop to represent the size of the avatar images to be shown */ + avatarSize?: typeof CONST.AVATAR_SIZE; - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle: boolean; - /** Avatars to show on the right of the menu item */ - floatRightAvatars: AvatarType; - - /** The type of brick road indicator to show. */ - brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + floatRightAvatars?: AvatarType[]; /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize: typeof CONST.AVATAR_SIZE; + floatRightAvatarSize?: typeof CONST.AVATAR_SIZE; - /** Prop to represent the size of the avatar images to be shown */ - avatarSize: typeof CONST.AVATAR_SIZE; +/** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu?: boolean; + // ------------------------------- VALID PROPS ABOVE + + /** The type of brick road indicator to show. */ + brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + /** The function that should be called when this component is LongPressed or right-clicked. */ onSecondaryInteraction: () => void; @@ -183,21 +201,12 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** The action accept for anonymous user or not */ isAnonymousAction: boolean; - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu: boolean; - /** Should we grey out the menu item when it is disabled? */ shouldGreyOutWhenDisabled: boolean; /** Should render the content in HTML format */ shouldRenderAsHTML: boolean; - /** Component to be displayed on the right */ - rightComponent: ReactNode; - - /** Should render component on the right */ - shouldShowRightComponent: boolean; - /** Array of objects that map display names to their corresponding tooltip */ titleWithTooltips: ReactNode[]; @@ -210,8 +219,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N // TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage const defaultProps = { - shouldShowBasicTitle: false, - shouldShowHeaderTitle: false, shouldParseTitle: false, descriptionTextStyle: styles.breakWord, interactive: true, @@ -221,11 +228,9 @@ const defaultProps = { shouldBlockSelection: false, furtherDetails: '', isAnonymousAction: false, - isSmallAvatarSubscriptMenu: false, numberOfLinesTitle: 1, shouldGreyOutWhenDisabled: true, shouldRenderAsHTML: false, - shouldShowRightComponent: false, titleWithTooltips: [], shouldCheckActionAllowedOnPress: true, }; @@ -235,10 +240,11 @@ function MenuItem({ icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, description, error, success = false, focused = false, disabled = false, - title, subtitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, - shouldShowDescriptionOnTop = false, + title, subtitle, shouldShowBasicTitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, + shouldShowDescriptionOnTop = false, shouldShowRightComponent = false, rightComponent, + floatRightAvatars = [], floatRightAvatarSize, avatarSize = CONST.AVATAR_SIZE.DEFAULT, isSmallAvatarSubscriptMenu = false, // Props not validated below - Validate if required and default value - shouldShowBasicTitle,interactive,floatRightAvatars,brickRoadIndicator = '',floatRightAvatarSize,avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,rightComponent,shouldShowRightComponent,titleWithTooltips, + interactive,brickRoadIndicator = '',avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -252,7 +258,6 @@ function MenuItem({ styles.popoverMenuText, icon && !_.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), shouldShowBasicTitle ? undefined : styles.textStrong, - shouldShowHeaderTitle ? styles.textHeadlineH1 : undefined, numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, interactive && disabled ? {...styles.userSelectNone} : undefined, styles.ltr, From 87db8e24ca9fd7f868b177e112f1ce12721b47db Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 16:56:52 +0100 Subject: [PATCH 030/244] Prop types -> Typescript props --- src/components/MenuItem.tsx | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8c7e426a4cfe..c53ab3c3bdb1 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -173,7 +173,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N shouldStackHorizontally: boolean; /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: typeof CONST.AVATAR_SIZE; + avatarSize?: typeof CONST.AVATAR_SIZE[keyof typeof CONST.AVATAR_SIZE]; /** Avatars to show on the right of the menu item */ floatRightAvatars?: AvatarType[]; @@ -181,13 +181,13 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** Prop to represent the size of the float right avatar images to be shown */ floatRightAvatarSize?: typeof CONST.AVATAR_SIZE; -/** Whether we should use small avatar subscript sizing the for menu item */ + /** Whether we should use small avatar subscript sizing the for menu item */ isSmallAvatarSubscriptMenu?: boolean; - // ------------------------------- VALID PROPS ABOVE - /** The type of brick road indicator to show. */ - brickRoadIndicator: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | ''; + brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS[keyof typeof CONST.BRICK_ROAD_INDICATOR_STATUS]; + + // ------------------------------- VALID PROPS ABOVE /** The function that should be called when this component is LongPressed or right-clicked. */ onSecondaryInteraction: () => void; @@ -221,10 +221,6 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N const defaultProps = { shouldParseTitle: false, descriptionTextStyle: styles.breakWord, - interactive: true, - brickRoadIndicator: '', - floatRightAvatars: [], - avatarSize: CONST.AVATAR_SIZE.DEFAULT, shouldBlockSelection: false, furtherDetails: '', isAnonymousAction: false, @@ -236,15 +232,16 @@ const defaultProps = { }; function MenuItem({ - badgeText, onPress, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, + interactive = true, onPress, badgeText, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, description, error, success = false, focused = false, disabled = false, title, subtitle, shouldShowBasicTitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, shouldShowDescriptionOnTop = false, shouldShowRightComponent = false, rightComponent, floatRightAvatars = [], floatRightAvatarSize, avatarSize = CONST.AVATAR_SIZE.DEFAULT, isSmallAvatarSubscriptMenu = false, + brickRoadIndicator, // Props not validated below - Validate if required and default value - interactive,brickRoadIndicator = '',avatarSize,onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,isSmallAvatarSubscriptMenu,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, + onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, shouldCheckActionAllowedOnPress }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -301,7 +298,7 @@ function MenuItem({ const hasPressableRightComponent = iconRight || (rightComponent && shouldShowRightComponent); const renderTitleContent = () => { - if (titleWithTooltips && _.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { + if (titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { return ( { From 3683fcb5a99470da05ce4ed96f21a3af7e31ae7d Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 10 Nov 2023 17:02:44 +0100 Subject: [PATCH 031/244] Prop types -> Typescript props --- src/components/MenuItem.tsx | 60 +++++++++++++------------------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index c53ab3c3bdb1..b5686926258a 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -187,62 +187,42 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** The type of brick road indicator to show. */ brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS[keyof typeof CONST.BRICK_ROAD_INDICATOR_STATUS]; - // ------------------------------- VALID PROPS ABOVE - - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; - - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection: boolean; + /** Should render the content in HTML format */ + shouldRenderAsHTML?: boolean; - /** Text to display under the main item */ - furtherDetails: string; + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled?: boolean; /** The action accept for anonymous user or not */ - isAnonymousAction: boolean; + isAnonymousAction?: boolean; - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled: boolean; + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection?: boolean; - /** Should render the content in HTML format */ - shouldRenderAsHTML: boolean; - - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: ReactNode[]; + shouldParseTitle?: false; /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress: boolean; -}; + shouldCheckActionAllowedOnPress?: boolean; -// TODO: Destructure props -// TODO: Adjust default values -// TODO: Adjust () => void in AvatarProps - always just used () => void without checking the usage - -const defaultProps = { - shouldParseTitle: false, - descriptionTextStyle: styles.breakWord, - shouldBlockSelection: false, - furtherDetails: '', - isAnonymousAction: false, - numberOfLinesTitle: 1, - shouldGreyOutWhenDisabled: true, - shouldRenderAsHTML: false, - titleWithTooltips: [], - shouldCheckActionAllowedOnPress: true, -}; + /** Text to display under the main item */ + furtherDetails?: string; + + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction: () => void; + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips: ReactNode[]; +}; function MenuItem({ interactive = true, onPress, badgeText, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, - shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, + shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, furtherDetails, description, error, success = false, focused = false, disabled = false, title, subtitle, shouldShowBasicTitle, label, shouldShowSelectedState = false, isSelected = false, shouldStackHorizontally = false, shouldShowDescriptionOnTop = false, shouldShowRightComponent = false, rightComponent, floatRightAvatars = [], floatRightAvatarSize, avatarSize = CONST.AVATAR_SIZE.DEFAULT, isSmallAvatarSubscriptMenu = false, - brickRoadIndicator, - // Props not validated below - Validate if required and default value - onSecondaryInteraction,shouldBlockSelection,furtherDetails,isAnonymousAction,shouldGreyOutWhenDisabled,shouldRenderAsHTML,titleWithTooltips, - shouldCheckActionAllowedOnPress + brickRoadIndicator, shouldRenderAsHTML = false, shouldGreyOutWhenDisabled = true, isAnonymousAction = false, + shouldBlockSelection = false, shouldParseTitle = false, shouldCheckActionAllowedOnPress = true, onSecondaryInteraction, titleWithTooltips }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); From bab1de41b8e579d609142d2364659c70c831fcd0 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 18:37:47 +0200 Subject: [PATCH 032/244] Move logic in a separate unit --- src/libs/ModifiedExpenseMessage.ts | 276 ++++++++++++++++++ .../LocalNotification/BrowserNotifications.js | 3 +- src/libs/OptionsListUtils.js | 3 +- src/libs/ReportUtils.js | 264 ----------------- .../report/ContextMenu/ContextMenuActions.js | 3 +- src/pages/home/report/ReportActionItem.js | 3 +- 6 files changed, 284 insertions(+), 268 deletions(-) create mode 100644 src/libs/ModifiedExpenseMessage.ts diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts new file mode 100644 index 000000000000..d323edcd38f8 --- /dev/null +++ b/src/libs/ModifiedExpenseMessage.ts @@ -0,0 +1,276 @@ +import {format} from 'date-fns'; +import lodashGet from 'lodash/get'; +import * as Localize from './Localize'; +import * as PolicyUtils from './PolicyUtils'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as ReportUtils from './ReportUtils'; +import * as CurrencyUtils from './CurrencyUtils'; +import _ from 'underscore'; + +let allPolicyTags = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +function getPolicyTags(policyID: string) { + return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); +} + + +/** + * Get the proper message schema for a modified field on the expense. + * + * @param {String} newValue + * @param {String} oldValue + * @param {String} valueName + * @param {Boolean} valueInQuotes + * @param {Boolean} shouldConvertToLowercase + * @returns {String} + */ + +function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { + const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; + const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; + const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; + + if (!oldValue) { + return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); + } + if (!newValue) { + return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); + } + return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); +} + +/** + * Get the proper message line for a modified expense. + * + * @param {String} newValue + * @param {String} oldValue + * @param {String} valueName + * @param {Boolean} valueInQuotes + * @returns {String} + */ + +function getProperLineForModifiedExpenseMessage(prefix: string, messageFragments: Array) { + if (messageFragments.length === 0) { + return ''; + } + return messageFragments.reduce((acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; + } + if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; + }, prefix); +} + +/** + * Get the proper message schema for modified distance message. + * + * @param {String} newDistance + * @param {String} oldDistance + * @param {String} newAmount + * @param {String} oldAmount + * @returns {String} + */ + +function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { + if (!oldDistance) { + return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); + } + return Localize.translateLocal('iou.updatedTheDistance', { + newDistanceToDisplay: newDistance, + oldDistanceToDisplay: oldDistance, + newAmountToDisplay: newAmount, + oldAmountToDisplay: oldAmount, + }); +} + +/** + * Get the report action message when expense has been modified. + * + * ModifiedExpense::getNewDotComment in Web-Expensify should match this. + * If we change this function be sure to update the backend as well. + * + * @param {Object} reportAction + * @returns {String} + */ +function getModifiedExpenseMessage(reportAction: Object): string { + const reportActionOriginalMessage: any = lodashGet(reportAction, 'originalMessage', {}); + if (_.isEmpty(reportActionOriginalMessage)) { + return Localize.translateLocal('iou.changedTheRequest'); + } + const reportID = lodashGet(reportAction, 'reportID', ''); + const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); + const policyTags = getPolicyTags(policyID); + const policyTag = PolicyUtils.getTag(policyTags); + const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); + + const removalFragments = []; + const setFragments = []; + const changeFragments = []; + + const hasModifiedAmount = + _.has(reportActionOriginalMessage, 'oldAmount') && + _.has(reportActionOriginalMessage, 'oldCurrency') && + _.has(reportActionOriginalMessage, 'amount') && + _.has(reportActionOriginalMessage, 'currency'); + + const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); + if (hasModifiedAmount) { + const oldCurrency = reportActionOriginalMessage.oldCurrency; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); + + const currency = reportActionOriginalMessage.currency; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); + + // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. + // We check the merchant is in distance format (includes @) as a sanity check + if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { + return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); + } + + const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + if (!oldAmount) { + setFragments.push(fragment); + } else if (!amount) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); + if (hasModifiedComment) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.newComment, + reportActionOriginalMessage.oldComment, + Localize.translateLocal('common.description'), + true, + ); + if (!reportActionOriginalMessage.oldComment) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.newComment) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); + if (hasModifiedCreated) { + // Take only the YYYY-MM-DD value as the original date includes timestamp + let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); + const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + if (!formattedOldCreated) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.created) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + if (hasModifiedMerchant) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.merchant, + reportActionOriginalMessage.oldMerchant, + Localize.translateLocal('common.merchant'), + true, + ); + if (!reportActionOriginalMessage.oldMerchant) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.merchant) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); + if (hasModifiedCategory) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.category, + reportActionOriginalMessage.oldCategory, + Localize.translateLocal('common.category'), + true, + ); + if (!reportActionOriginalMessage.oldCategory) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.category) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); + if (hasModifiedTag) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.tag, + reportActionOriginalMessage.oldTag, + policyTagListName, + true, + policyTagListName === Localize.translateLocal('common.tag'), + ); + if (!reportActionOriginalMessage.oldTag) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.tag) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); + if (hasModifiedBillable) { + const fragment = getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage.billable, + reportActionOriginalMessage.oldBillable, + Localize.translateLocal('iou.request'), + true, + ); + if (!reportActionOriginalMessage.oldBillable) { + setFragments.push(fragment); + } else if (!reportActionOriginalMessage.billable) { + removalFragments.push(fragment); + } else { + changeFragments.push(fragment); + } + } + + let message = + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); + if (message === '') { + return message; + } + message = `${message.substring(1, message.length)}`; + return message; +} + +export default { + getModifiedExpenseMessage, +}; \ No newline at end of file diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index 20d9be48d915..497b686e7d5e 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -4,6 +4,7 @@ import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.p import * as ReportUtils from '@libs/ReportUtils'; import * as AppUpdate from '@userActions/AppUpdate'; import focusApp from './focusApp'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const DEFAULT_DELAY = 4000; @@ -131,7 +132,7 @@ export default { pushModifiedExpenseNotification({reportAction, onClick}, usesIcon = false) { push({ title: _.map(reportAction.person, (f) => f.text).join(', '), - body: ReportUtils.getModifiedExpenseMessage(reportAction), + body: ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction), delay: 0, onClick, icon: usesIcon ? EXPENSIFY_ICON_URL : '', diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 7e0aaa8ffb2f..af0407a18112 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -20,6 +20,7 @@ import * as ReportActionUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +import ModifiedExpenseMessage from './ModifiedExpenseMessage'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -402,7 +403,7 @@ function getLastMessageTextForReport(report) { } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction); + const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 4968fc33b04b..2a3db8437518 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1,5 +1,4 @@ /* eslint-disable rulesdir/prefer-underscore-method */ -import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; @@ -81,25 +80,6 @@ Onyx.connect({ callback: (val) => (loginList = val), }); -let allPolicyTags = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - -function getPolicyTags(policyID) { - return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); -} - function getChatType(report) { return report ? report.chatType : ''; } @@ -1884,249 +1864,6 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); } -/** - * Get the proper message schema for a modified field on the expense. - * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @param {Boolean} shouldConvertToLowercase - * @returns {String} - */ - -function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, valueInQuotes, shouldConvertToLowercase = true) { - const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; - const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; - const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; - - if (!oldValue) { - return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); - } - if (!newValue) { - return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); - } - return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); -} - -/** - * Get the proper message line for a modified expense. - * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @returns {String} - */ - -function getProperLineForModifiedExpenseMessage(prefix, messageFragments) { - if (messageFragments.length === 0) { - return ''; - } - return messageFragments.reduce((acc, value, index) => { - if (index === messageFragments.length - 1) { - if (messageFragments.length === 1) { - return `${acc} ${value}.`; - } - if (messageFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, prefix); -} - -/** - * Get the proper message schema for modified distance message. - * - * @param {String} newDistance - * @param {String} oldDistance - * @param {String} newAmount - * @param {String} oldAmount - * @returns {String} - */ - -function getProperSchemaForModifiedDistanceMessage(newDistance, oldDistance, newAmount, oldAmount) { - if (!oldDistance) { - return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); - } - return Localize.translateLocal('iou.updatedTheDistance', { - newDistanceToDisplay: newDistance, - oldDistanceToDisplay: oldDistance, - newAmountToDisplay: newAmount, - oldAmountToDisplay: oldAmount, - }); -} - -/** - * Get the report action message when expense has been modified. - * - * ModifiedExpense::getNewDotComment in Web-Expensify should match this. - * If we change this function be sure to update the backend as well. - * - * @param {Object} reportAction - * @returns {String} - */ -function getModifiedExpenseMessage(reportAction) { - const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {}); - if (_.isEmpty(reportActionOriginalMessage)) { - return Localize.translateLocal('iou.changedTheRequest'); - } - const reportID = lodashGet(reportAction, 'reportID', ''); - const policyID = lodashGet(getReport(reportID), 'policyID', ''); - const policyTags = getPolicyTags(policyID); - const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); - - const removalFragments = []; - const setFragments = []; - const changeFragments = []; - - const hasModifiedAmount = - _.has(reportActionOriginalMessage, 'oldAmount') && - _.has(reportActionOriginalMessage, 'oldCurrency') && - _.has(reportActionOriginalMessage, 'amount') && - _.has(reportActionOriginalMessage, 'currency'); - - const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); - if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); - - const currency = reportActionOriginalMessage.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); - - // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. - // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); - } - - const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); - if (!oldAmount) { - setFragments.push(fragment); - } else if (!amount) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); - if (hasModifiedComment) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.newComment, - reportActionOriginalMessage.oldComment, - Localize.translateLocal('common.description'), - true, - ); - if (!reportActionOriginalMessage.oldComment) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.newComment) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); - if (hasModifiedCreated) { - // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated); - formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); - if (!formattedOldCreated) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.created) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - if (hasModifiedMerchant) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.merchant, - reportActionOriginalMessage.oldMerchant, - Localize.translateLocal('common.merchant'), - true, - ); - if (!reportActionOriginalMessage.oldMerchant) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.merchant) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); - if (hasModifiedCategory) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.category, - reportActionOriginalMessage.oldCategory, - Localize.translateLocal('common.category'), - true, - ); - if (!reportActionOriginalMessage.oldCategory) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.category) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); - if (hasModifiedTag) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.tag, - reportActionOriginalMessage.oldTag, - policyTagListName, - true, - policyTagListName === Localize.translateLocal('common.tag'), - ); - if (!reportActionOriginalMessage.oldTag) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.tag) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); - if (hasModifiedBillable) { - const fragment = getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.billable, - reportActionOriginalMessage.oldBillable, - Localize.translateLocal('iou.request'), - true, - ); - if (!reportActionOriginalMessage.oldBillable) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.billable) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } - } - - let message = - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); - if (message === '') { - return message; - } - message = `${message.substring(1, message.length)}`; - return message; -} - /** * Given the updates user made to the request, compose the originalMessage * object of the modified expense action. @@ -4514,7 +4251,6 @@ export { getParentReport, getRootParentReport, getReportPreviewMessage, - getModifiedExpenseMessage, canUserPerformWriteAction, getOriginalReportID, canAccessReport, diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 4f35926c5957..0d788dbefda4 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -22,6 +22,7 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; /** * Gets the HTML version of the message in an action. @@ -276,7 +277,7 @@ export default [ const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction); Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = ReportUtils.getModifiedExpenseMessage(reportAction); + const modifyExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction); Clipboard.setString(modifyExpenseMessage); } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9f803f72cbbb..ed89e4c2f413 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -74,6 +74,7 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const propTypes = { ...windowDimensionsPropTypes, @@ -414,7 +415,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { - children = ; + children = ; } else { const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); children = ( From 4ea44ee4d1c1be7f8d79ee0d8388278262d5b681 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 18:43:45 +0200 Subject: [PATCH 033/244] Move policy tags logic to PolicyUtils --- src/libs/ModifiedExpenseMessage.ts | 34 ++++++------------------------ src/libs/PolicyUtils.ts | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index d323edcd38f8..18767c68e010 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -2,42 +2,20 @@ import {format} from 'date-fns'; import lodashGet from 'lodash/get'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; -import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import * as ReportUtils from './ReportUtils'; import * as CurrencyUtils from './CurrencyUtils'; import _ from 'underscore'; -let allPolicyTags = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - -function getPolicyTags(policyID: string) { - return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); -} - /** * Get the proper message schema for a modified field on the expense. * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @param {Boolean} shouldConvertToLowercase - * @returns {String} + * @param newValue + * @param oldValue + * @param valueName + * @param valueInQuotes + * @param shouldConvertToLowercase */ function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { @@ -123,7 +101,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { } const reportID = lodashGet(reportAction, 'reportID', ''); const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); - const policyTags = getPolicyTags(policyID); + const policyTags = PolicyUtils.getPolicyTags(policyID); const policyTag = PolicyUtils.getTag(policyTags); const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 62640a11311a..aa33a74dedf9 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -3,11 +3,32 @@ import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '@src/types/onyx'; +import Onyx from 'react-native-onyx'; +import lodashGet from 'lodash/get'; type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; type UnitRate = {rate: number}; +let allPolicyTags = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +function getPolicyTags(policyID: string) { + return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); +} + /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. @@ -199,6 +220,7 @@ function isPendingDeletePolicy(policy: OnyxEntry): boolean { export { getActivePolicies, + getPolicyTags, hasPolicyMemberError, hasPolicyError, hasPolicyErrorFields, From b9229a774e6919d00cc553c2866a11d02ed1d68d Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 22:35:25 +0200 Subject: [PATCH 034/244] Better naming --- src/libs/ModifiedExpenseMessage.ts | 59 +++++++++---------- .../LocalNotification/BrowserNotifications.js | 2 +- src/libs/OptionsListUtils.js | 2 +- .../report/ContextMenu/ContextMenuActions.js | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 18767c68e010..f95c2ddfef99 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -9,7 +9,7 @@ import _ from 'underscore'; /** - * Get the proper message schema for a modified field on the expense. + * Get the partial message for a modified field on the expense. * * @param newValue * @param oldValue @@ -18,7 +18,7 @@ import _ from 'underscore'; * @param shouldConvertToLowercase */ -function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { +function getPartialMessageForValue(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; @@ -33,16 +33,15 @@ function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: st } /** - * Get the proper message line for a modified expense. + * Get the message line for a modified expense. * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @returns {String} + * @param newValue + * @param oldValue + * @param valueName + * @param valueInQuotes */ -function getProperLineForModifiedExpenseMessage(prefix: string, messageFragments: Array) { +function getMessageLine(prefix: string, messageFragments: Array) { if (messageFragments.length === 0) { return ''; } @@ -64,16 +63,15 @@ function getProperLineForModifiedExpenseMessage(prefix: string, messageFragments } /** - * Get the proper message schema for modified distance message. + * Get the message for a modified distance request. * - * @param {String} newDistance - * @param {String} oldDistance - * @param {String} newAmount - * @param {String} oldAmount - * @returns {String} + * @param newDistance + * @param oldDistance + * @param newAmount + * @param oldAmount */ -function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { +function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -91,10 +89,9 @@ function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDista * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. * - * @param {Object} reportAction - * @returns {String} + * @param reportAction */ -function getModifiedExpenseMessage(reportAction: Object): string { +function getForReportAction(reportAction: Object): string { const reportActionOriginalMessage: any = lodashGet(reportAction, 'originalMessage', {}); if (_.isEmpty(reportActionOriginalMessage)) { return Localize.translateLocal('iou.changedTheRequest'); @@ -126,10 +123,10 @@ function getModifiedExpenseMessage(reportAction: Object): string { // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. // We check the merchant is in distance format (includes @) as a sanity check if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); + return getForDistanceRequest(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - const fragment = getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); + const fragment = getPartialMessageForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false); if (!oldAmount) { setFragments.push(fragment); } else if (!amount) { @@ -141,7 +138,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), @@ -160,7 +157,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); - const fragment = getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + const fragment = getPartialMessageForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); if (!formattedOldCreated) { setFragments.push(fragment); } else if (!reportActionOriginalMessage.created) { @@ -171,7 +168,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { } if (hasModifiedMerchant) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), @@ -188,7 +185,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), @@ -205,7 +202,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, policyTagListName, @@ -223,7 +220,7 @@ function getModifiedExpenseMessage(reportAction: Object): string { const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - const fragment = getProperSchemaForModifiedExpenseMessage( + const fragment = getPartialMessageForValue( reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), @@ -239,9 +236,9 @@ function getModifiedExpenseMessage(reportAction: Object): string { } let message = - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.set')}`, setFragments) + - getProperLineForModifiedExpenseMessage(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); + getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getMessageLine(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { return message; } @@ -250,5 +247,5 @@ function getModifiedExpenseMessage(reportAction: Object): string { } export default { - getModifiedExpenseMessage, + getForReportAction, }; \ No newline at end of file diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index 497b686e7d5e..853106adafe8 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -132,7 +132,7 @@ export default { pushModifiedExpenseNotification({reportAction, onClick}, usesIcon = false) { push({ title: _.map(reportAction.person, (f) => f.text).join(', '), - body: ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction), + body: ModifiedExpenseMessage.getForReportAction(reportAction), delay: 0, onClick, icon: usesIcon ? EXPENSIFY_ICON_URL : '', diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index af0407a18112..fd6031f5c901 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -403,7 +403,7 @@ function getLastMessageTextForReport(report) { } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(lastReportAction); + const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 0d788dbefda4..bebb2e2688c5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -277,7 +277,7 @@ export default [ const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction); Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = ModifiedExpenseMessage.getModifiedExpenseMessage(reportAction); + const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportAction); Clipboard.setString(modifyExpenseMessage); } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index ed89e4c2f413..ce4c151bf2d6 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -415,7 +415,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { - children = ; + children = ; } else { const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); children = ( From 72714adfc3730fd32f9e2d551b0ad9ed8016de8b Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 22:44:31 +0200 Subject: [PATCH 035/244] Dry up code. --- src/libs/ModifiedExpenseMessage.ts | 103 ++++++++++------------------- 1 file changed, 34 insertions(+), 69 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index f95c2ddfef99..d3f0b07cbcb8 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -9,7 +9,7 @@ import _ from 'underscore'; /** - * Get the partial message for a modified field on the expense. + * Builds the partial message fragment for a modified field on the expense. * * @param newValue * @param oldValue @@ -18,18 +18,30 @@ import _ from 'underscore'; * @param shouldConvertToLowercase */ -function getPartialMessageForValue(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true) { +function buildMessageFragmentForValue( + newValue: string, + oldValue: string, + valueName: string, + valueInQuotes: boolean, + setFragments: Array, + removalFragments: Array, + changeFragments: Array, + shouldConvertToLowercase = true +) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; if (!oldValue) { - return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); + const fragment = Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); + setFragments.push(fragment); + } else if (!newValue) { + const fragment = Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); + removalFragments.push(fragment); + } else { + const fragment = Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); + changeFragments.push(fragment); } - if (!newValue) { - return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); - } - return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); } /** @@ -102,9 +114,9 @@ function getForReportAction(reportAction: Object): string { const policyTag = PolicyUtils.getTag(policyTags); const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); - const removalFragments = []; - const setFragments = []; - const changeFragments = []; + const removalFragments: Array = []; + const setFragments: Array = []; + const changeFragments: Array = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -126,113 +138,66 @@ function getForReportAction(reportAction: Object): string { return getForDistanceRequest(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); } - const fragment = getPartialMessageForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false); - if (!oldAmount) { - setFragments.push(fragment); - } else if (!amount) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } + buildMessageFragmentForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false, setFragments, removalFragments, changeFragments); } const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); if (hasModifiedComment) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), - true, + true, setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldComment) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.newComment) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); - const fragment = getPartialMessageForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); - if (!formattedOldCreated) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.created) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } + buildMessageFragmentForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false, setFragments, removalFragments, changeFragments); } if (hasModifiedMerchant) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true, + setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldMerchant) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.merchant) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); if (hasModifiedCategory) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), - true, + true, setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldCategory) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.category) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); if (hasModifiedTag) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, policyTagListName, true, + setFragments, removalFragments, changeFragments, policyTagListName === Localize.translateLocal('common.tag'), ); - if (!reportActionOriginalMessage.oldTag) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.tag) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); if (hasModifiedBillable) { - const fragment = getPartialMessageForValue( + buildMessageFragmentForValue( reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), - true, + true, setFragments, removalFragments, changeFragments ); - if (!reportActionOriginalMessage.oldBillable) { - setFragments.push(fragment); - } else if (!reportActionOriginalMessage.billable) { - removalFragments.push(fragment); - } else { - changeFragments.push(fragment); - } } let message = From 18d1bf0fb300c9eac3c69ec8ff4e72d8097a0f77 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Mon, 20 Nov 2023 22:50:10 +0200 Subject: [PATCH 036/244] Run prettier --- src/libs/ModifiedExpenseMessage.ts | 66 ++++++++++++------- .../LocalNotification/BrowserNotifications.js | 2 +- src/libs/OptionsListUtils.js | 2 +- src/libs/PolicyUtils.ts | 4 +- .../report/ContextMenu/ContextMenuActions.js | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index d3f0b07cbcb8..d0ebce1275b5 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,12 +1,11 @@ import {format} from 'date-fns'; import lodashGet from 'lodash/get'; +import _ from 'underscore'; +import CONST from '@src/CONST'; +import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; -import CONST from '@src/CONST'; import * as ReportUtils from './ReportUtils'; -import * as CurrencyUtils from './CurrencyUtils'; -import _ from 'underscore'; - /** * Builds the partial message fragment for a modified field on the expense. @@ -19,14 +18,14 @@ import _ from 'underscore'; */ function buildMessageFragmentForValue( - newValue: string, - oldValue: string, - valueName: string, - valueInQuotes: boolean, - setFragments: Array, - removalFragments: Array, - changeFragments: Array, - shouldConvertToLowercase = true + newValue: string, + oldValue: string, + valueName: string, + valueInQuotes: boolean, + setFragments: string[], + removalFragments: string[], + changeFragments: string[], + shouldConvertToLowercase = true, ) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; @@ -53,7 +52,7 @@ function buildMessageFragmentForValue( * @param valueInQuotes */ -function getMessageLine(prefix: string, messageFragments: Array) { +function getMessageLine(prefix: string, messageFragments: string[]) { if (messageFragments.length === 0) { return ''; } @@ -114,9 +113,9 @@ function getForReportAction(reportAction: Object): string { const policyTag = PolicyUtils.getTag(policyTags); const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); - const removalFragments: Array = []; - const setFragments: Array = []; - const changeFragments: Array = []; + const removalFragments: string[] = []; + const setFragments: string[] = []; + const changeFragments: string[] = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -147,7 +146,10 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), - true, setFragments, removalFragments, changeFragments + true, + setFragments, + removalFragments, + changeFragments, ); } @@ -155,7 +157,15 @@ function getForReportAction(reportAction: Object): string { if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); - buildMessageFragmentForValue(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false, setFragments, removalFragments, changeFragments); + buildMessageFragmentForValue( + reportActionOriginalMessage.created, + formattedOldCreated, + Localize.translateLocal('common.date'), + false, + setFragments, + removalFragments, + changeFragments, + ); } if (hasModifiedMerchant) { @@ -164,7 +174,9 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true, - setFragments, removalFragments, changeFragments + setFragments, + removalFragments, + changeFragments, ); } @@ -174,7 +186,10 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), - true, setFragments, removalFragments, changeFragments + true, + setFragments, + removalFragments, + changeFragments, ); } @@ -185,7 +200,9 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.oldTag, policyTagListName, true, - setFragments, removalFragments, changeFragments, + setFragments, + removalFragments, + changeFragments, policyTagListName === Localize.translateLocal('common.tag'), ); } @@ -196,7 +213,10 @@ function getForReportAction(reportAction: Object): string { reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), - true, setFragments, removalFragments, changeFragments + true, + setFragments, + removalFragments, + changeFragments, ); } @@ -213,4 +233,4 @@ function getForReportAction(reportAction: Object): string { export default { getForReportAction, -}; \ No newline at end of file +}; diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index 853106adafe8..4c990a87878c 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -1,10 +1,10 @@ // Web and desktop implementation only. Do not import for direct use. Use LocalNotification. import _ from 'underscore'; import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import * as ReportUtils from '@libs/ReportUtils'; import * as AppUpdate from '@userActions/AppUpdate'; import focusApp from './focusApp'; -import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const DEFAULT_DELAY = 4000; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index fd6031f5c901..9f0780c6a909 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -13,6 +13,7 @@ import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; +import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; @@ -20,7 +21,6 @@ import * as ReportActionUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -import ModifiedExpenseMessage from './ModifiedExpenseMessage'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index aa33a74dedf9..ead8d20f838e 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,10 +1,10 @@ import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '@src/types/onyx'; -import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index bebb2e2688c5..8be004cbe6a5 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -10,6 +10,7 @@ import Clipboard from '@libs/Clipboard'; import * as Environment from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -22,7 +23,6 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; -import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; /** * Gets the HTML version of the message in an action. diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index ce4c151bf2d6..6ae2241093e4 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -36,6 +36,7 @@ import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -74,7 +75,6 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; const propTypes = { ...windowDimensionsPropTypes, From 5444abd6030d0c45df9a3ff0a4c88b432f234ecf Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 01:33:08 +0200 Subject: [PATCH 037/244] Fix lint errors --- ...seMessage.ts => ModifiedExpenseMessage.js} | 64 +++++++++---------- src/libs/PolicyUtils.ts | 13 ++-- 2 files changed, 34 insertions(+), 43 deletions(-) rename src/libs/{ModifiedExpenseMessage.ts => ModifiedExpenseMessage.js} (82%) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.js similarity index 82% rename from src/libs/ModifiedExpenseMessage.ts rename to src/libs/ModifiedExpenseMessage.js index d0ebce1275b5..5428413efb96 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.js @@ -17,16 +17,7 @@ import * as ReportUtils from './ReportUtils'; * @param shouldConvertToLowercase */ -function buildMessageFragmentForValue( - newValue: string, - oldValue: string, - valueName: string, - valueInQuotes: boolean, - setFragments: string[], - removalFragments: string[], - changeFragments: string[], - shouldConvertToLowercase = true, -) { +function buildMessageFragmentForValue(newValue, oldValue, valueName, valueInQuotes, setFragments, removalFragments, changeFragments, shouldConvertToLowercase = true) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; @@ -52,25 +43,29 @@ function buildMessageFragmentForValue( * @param valueInQuotes */ -function getMessageLine(prefix: string, messageFragments: string[]) { +function getMessageLine(prefix, messageFragments) { if (messageFragments.length === 0) { return ''; } - return messageFragments.reduce((acc, value, index) => { - if (index === messageFragments.length - 1) { - if (messageFragments.length === 1) { - return `${acc} ${value}.`; + return _.reduce( + messageFragments, + (acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; + } + if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; } - if (messageFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + if (index === 0) { + return `${acc} ${value}`; } - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; - } - if (index === 0) { - return `${acc} ${value}`; - } - return `${acc}, ${value}`; - }, prefix); + return `${acc}, ${value}`; + }, + prefix, + ); } /** @@ -80,9 +75,10 @@ function getMessageLine(prefix: string, messageFragments: string[]) { * @param oldDistance * @param newAmount * @param oldAmount + * @returns {String} */ -function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { +function getForDistanceRequest(newDistance, oldDistance, newAmount, oldAmount) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -100,22 +96,22 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. * - * @param reportAction + * @param {Object} reportAction + * @returns {String} */ -function getForReportAction(reportAction: Object): string { - const reportActionOriginalMessage: any = lodashGet(reportAction, 'originalMessage', {}); +function getForReportAction(reportAction) { + const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {}); if (_.isEmpty(reportActionOriginalMessage)) { return Localize.translateLocal('iou.changedTheRequest'); } const reportID = lodashGet(reportAction, 'reportID', ''); const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); const policyTags = PolicyUtils.getPolicyTags(policyID); - const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); + const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); - const removalFragments: string[] = []; - const setFragments: string[] = []; - const changeFragments: string[] = []; + const removalFragments = []; + const setFragments = []; + const changeFragments = []; const hasModifiedAmount = _.has(reportActionOriginalMessage, 'oldAmount') && @@ -156,7 +152,7 @@ function getForReportAction(reportAction: Object): string { const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); + const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); buildMessageFragmentForValue( reportActionOriginalMessage.created, formattedOldCreated, diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index ead8d20f838e..ee1243cc573d 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,7 +1,5 @@ import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '@src/types/onyx'; @@ -10,23 +8,20 @@ type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; type UnitRate = {rate: number}; -let allPolicyTags = {}; - +let allPolicyTags: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, callback: (value) => { if (!value) { - allPolicyTags = {}; return; } - - allPolicyTags = value; + allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); }, }); function getPolicyTags(policyID: string) { - return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; } /** From 371f138602ffb8f5ce4fb72d3d060bd46738e6ec Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 02:04:39 +0200 Subject: [PATCH 038/244] Make it a ts file. --- src/libs/{ModifiedExpenseMessage.js => ModifiedExpenseMessage.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/libs/{ModifiedExpenseMessage.js => ModifiedExpenseMessage.ts} (100%) diff --git a/src/libs/ModifiedExpenseMessage.js b/src/libs/ModifiedExpenseMessage.ts similarity index 100% rename from src/libs/ModifiedExpenseMessage.js rename to src/libs/ModifiedExpenseMessage.ts From ceaa9156cf5b12284654b0c790509e9ee0423933 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 02:49:30 +0200 Subject: [PATCH 039/244] Migrate to TS --- src/libs/ModifiedExpenseMessage.ts | 120 ++++++++++++++--------------- src/types/onyx/OriginalMessage.ts | 21 ++++- 2 files changed, 79 insertions(+), 62 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 5428413efb96..6ac50cc2984a 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,7 +1,7 @@ import {format} from 'date-fns'; import lodashGet from 'lodash/get'; -import _ from 'underscore'; import CONST from '@src/CONST'; +import {ReportAction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; @@ -17,7 +17,16 @@ import * as ReportUtils from './ReportUtils'; * @param shouldConvertToLowercase */ -function buildMessageFragmentForValue(newValue, oldValue, valueName, valueInQuotes, setFragments, removalFragments, changeFragments, shouldConvertToLowercase = true) { +function buildMessageFragmentForValue( + newValue: string, + oldValue: string, + valueName: string, + valueInQuotes: boolean, + setFragments: string[], + removalFragments: string[], + changeFragments: string[], + shouldConvertToLowercase = true, +) { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; @@ -43,29 +52,25 @@ function buildMessageFragmentForValue(newValue, oldValue, valueName, valueInQuot * @param valueInQuotes */ -function getMessageLine(prefix, messageFragments) { +function getMessageLine(prefix: string, messageFragments: string[]) { if (messageFragments.length === 0) { return ''; } - return _.reduce( - messageFragments, - (acc, value, index) => { - if (index === messageFragments.length - 1) { - if (messageFragments.length === 1) { - return `${acc} ${value}.`; - } - if (messageFragments.length === 2) { - return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; - } - return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + return messageFragments.reduce((acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; } - if (index === 0) { - return `${acc} ${value}`; + if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; } - return `${acc}, ${value}`; - }, - prefix, - ); + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; + }, prefix); } /** @@ -78,7 +83,7 @@ function getMessageLine(prefix, messageFragments) { * @returns {String} */ -function getForDistanceRequest(newDistance, oldDistance, newAmount, oldAmount) { +function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -96,51 +101,46 @@ function getForDistanceRequest(newDistance, oldDistance, newAmount, oldAmount) { * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. * - * @param {Object} reportAction - * @returns {String} + * @param reportAction */ -function getForReportAction(reportAction) { - const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {}); - if (_.isEmpty(reportActionOriginalMessage)) { - return Localize.translateLocal('iou.changedTheRequest'); +function getForReportAction(reportAction: ReportAction) { + if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { + return ''; } + const reportActionOriginalMessage = reportAction.originalMessage; const reportID = lodashGet(reportAction, 'reportID', ''); const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); const policyTags = PolicyUtils.getPolicyTags(policyID); const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); - const removalFragments = []; - const setFragments = []; - const changeFragments = []; + const removalFragments: string[] = []; + const setFragments: string[] = []; + const changeFragments: string[] = []; - const hasModifiedAmount = - _.has(reportActionOriginalMessage, 'oldAmount') && - _.has(reportActionOriginalMessage, 'oldCurrency') && - _.has(reportActionOriginalMessage, 'amount') && - _.has(reportActionOriginalMessage, 'currency'); + const hasModifiedAmount = reportActionOriginalMessage.oldAmount && reportActionOriginalMessage.oldCurrency && reportActionOriginalMessage.amount && reportActionOriginalMessage.currency; - const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); + const hasModifiedMerchant = reportActionOriginalMessage.oldMerchant && reportActionOriginalMessage.merchant; if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); + const oldCurrency = reportActionOriginalMessage.oldCurrency ?? ''; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount ?? 0, oldCurrency); - const currency = reportActionOriginalMessage.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); + const currency = reportActionOriginalMessage.currency ?? ''; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount ?? 0, currency); // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { - return getForDistanceRequest(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); + if (hasModifiedMerchant && (reportActionOriginalMessage.merchant ?? '').includes('@')) { + return getForDistanceRequest(reportActionOriginalMessage.merchant ?? '', reportActionOriginalMessage.oldMerchant ?? '', amount, oldAmount); } buildMessageFragmentForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false, setFragments, removalFragments, changeFragments); } - const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); + const hasModifiedComment = reportActionOriginalMessage.oldComment && reportActionOriginalMessage.newComment; if (hasModifiedComment) { buildMessageFragmentForValue( - reportActionOriginalMessage.newComment, - reportActionOriginalMessage.oldComment, + reportActionOriginalMessage.newComment ?? '', + reportActionOriginalMessage.oldComment ?? '', Localize.translateLocal('common.description'), true, setFragments, @@ -149,12 +149,12 @@ function getForReportAction(reportAction) { ); } - const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); + const hasModifiedCreated = reportActionOriginalMessage.oldCreated && reportActionOriginalMessage.created; if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated), CONST.DATE.FNS_FORMAT_STRING); + const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated ?? ''), CONST.DATE.FNS_FORMAT_STRING); buildMessageFragmentForValue( - reportActionOriginalMessage.created, + reportActionOriginalMessage.created ?? '', formattedOldCreated, Localize.translateLocal('common.date'), false, @@ -166,8 +166,8 @@ function getForReportAction(reportAction) { if (hasModifiedMerchant) { buildMessageFragmentForValue( - reportActionOriginalMessage.merchant, - reportActionOriginalMessage.oldMerchant, + reportActionOriginalMessage.merchant ?? '', + reportActionOriginalMessage.oldMerchant ?? '', Localize.translateLocal('common.merchant'), true, setFragments, @@ -176,11 +176,11 @@ function getForReportAction(reportAction) { ); } - const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); + const hasModifiedCategory = reportActionOriginalMessage.oldCategory && reportActionOriginalMessage.category; if (hasModifiedCategory) { buildMessageFragmentForValue( - reportActionOriginalMessage.category, - reportActionOriginalMessage.oldCategory, + reportActionOriginalMessage.category ?? '', + reportActionOriginalMessage.oldCategory ?? '', Localize.translateLocal('common.category'), true, setFragments, @@ -189,11 +189,11 @@ function getForReportAction(reportAction) { ); } - const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); + const hasModifiedTag = reportActionOriginalMessage.oldTag && reportActionOriginalMessage.tag; if (hasModifiedTag) { buildMessageFragmentForValue( - reportActionOriginalMessage.tag, - reportActionOriginalMessage.oldTag, + reportActionOriginalMessage.tag ?? '', + reportActionOriginalMessage.oldTag ?? '', policyTagListName, true, setFragments, @@ -203,11 +203,11 @@ function getForReportAction(reportAction) { ); } - const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); + const hasModifiedBillable = reportActionOriginalMessage.oldBillable && reportActionOriginalMessage.billable; if (hasModifiedBillable) { buildMessageFragmentForValue( - reportActionOriginalMessage.billable, - reportActionOriginalMessage.oldBillable, + reportActionOriginalMessage.billable ?? '', + reportActionOriginalMessage.oldBillable ?? '', Localize.translateLocal('iou.request'), true, setFragments, @@ -221,7 +221,7 @@ function getForReportAction(reportAction) { getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) + getMessageLine(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { - return message; + return Localize.translateLocal('iou.changedTheRequest'); } message = `${message.substring(1, message.length)}`; return message; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index b5e4b25a6508..c8ee5d5771de 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -171,7 +171,24 @@ type OriginalMessagePolicyTask = { type OriginalMessageModifiedExpense = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; - originalMessage: unknown; + originalMessage: { + oldMerchant?: string; + merchant?: string; + oldCurrency?: string; + currency?: string; + oldAmount?: number; + amount?: number; + oldComment?: string; + newComment?: string; + oldCreated?: string; + created?: string; + oldCategory?: string; + category?: string; + oldTag?: string; + tag?: string; + oldBillable?: string; + billable?: string; + }; }; type OriginalMessageReimbursementQueued = { @@ -196,4 +213,4 @@ type OriginalMessage = | OriginalMessageReimbursementQueued; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, OriginalMessageModifiedExpense}; From 3bec4fd1f562b596cbb6f327f77aee3a210e8ab6 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Tue, 21 Nov 2023 03:42:23 +0200 Subject: [PATCH 040/244] Make Lint happy. --- src/libs/ModifiedExpenseMessage.ts | 5 +---- src/libs/ReportUtils.js | 11 +++++++++++ src/types/onyx/OriginalMessage.ts | 2 +- src/types/onyx/ReportAction.ts | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 6ac50cc2984a..44259180f4eb 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,5 +1,4 @@ import {format} from 'date-fns'; -import lodashGet from 'lodash/get'; import CONST from '@src/CONST'; import {ReportAction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; @@ -80,7 +79,6 @@ function getMessageLine(prefix: string, messageFragments: string[]) { * @param oldDistance * @param newAmount * @param oldAmount - * @returns {String} */ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { @@ -108,8 +106,7 @@ function getForReportAction(reportAction: ReportAction) { return ''; } const reportActionOriginalMessage = reportAction.originalMessage; - const reportID = lodashGet(reportAction, 'reportID', ''); - const policyID = lodashGet(ReportUtils.getReport(reportID), 'policyID', ''); + const policyID = ReportUtils.getReportPolicyID(reportAction.reportID ?? ''); const policyTags = PolicyUtils.getPolicyTags(policyID); const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 2a3db8437518..248df849ca52 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -823,6 +823,16 @@ function getReport(reportID) { return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; } +/** + * Get the report policyID given a reportID + * + * @param {String} reportID + * @returns {String} + */ +function getReportPolicyID(reportID) { + return getReport(reportID).policyID || ''; +} + /** * Get the notification preference given a report * @@ -4173,6 +4183,7 @@ export { getDisplayNamesWithTooltips, getDisplayNamesStringFromTooltips, getReportName, + getReportPolicyID, getReport, getReportNotificationPreference, getReportIDFromLink, diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c8ee5d5771de..5e1bdfb7cf81 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -213,4 +213,4 @@ type OriginalMessage = | OriginalMessageReimbursementQueued; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, OriginalMessageModifiedExpense}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName}; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 2195c4e3ff0b..e67c075e54f1 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -90,6 +90,7 @@ type ReportActionBase = { avatar?: string; automatic?: boolean; shouldShow?: boolean; + reportID?: string; childReportID?: string; childReportName?: string; childType?: string; From 8802892701b70a68208bb0c54e67f42f9e6ef1c8 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Tue, 21 Nov 2023 10:25:11 +0100 Subject: [PATCH 041/244] Get rid of too many union types --- src/components/MenuItem.tsx | 67 ++++++++++++------------------------- 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index d9b8489ca7d3..12f9aa36c913 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -44,50 +44,7 @@ type UnresponsiveProps = { interactive: false; } -type TitleIconProps = { - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon: true; - - /** Icon to display at right side of title */ - titleIcon: IconType; -} - -type NoTitleIconProps = { - shouldShowTitleIcon?: false; - - titleIcon?: undefined; -} - -type RightIconProps = { - /** Boolean whether to display the right icon */ - shouldShowRightIcon: true; - - /** Overrides the icon for shouldShowRightIcon */ - iconRight: IconType; -} - -type NoRightIconProps = { - shouldShowRightIcon?: false; - - iconRight?: IconType; -} - -type RightComponent = { - /** Should render component on the right */ - shouldShowRightComponent: true; - - /** Component to be displayed on the right */ - rightComponent: ReactNode; -} - -type NoRightComponent = { - shouldShowRightComponent?: false; - - rightComponent?: undefined; -} - -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | NoTitleIconProps) & -(RightIconProps | NoRightIconProps) & (RightComponent | NoRightComponent) & { +type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Text to be shown as badge near the right end. */ badgeText?: string; @@ -133,6 +90,24 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & (TitleIconProps | N /** An icon to display under the main item */ furtherDetailsIcon?: IconType; + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon?: boolean; + + /** Icon to display at right side of title */ + titleIcon?: IconType; + + /** Boolean whether to display the right icon */ + shouldShowRightIcon?: boolean; + + /** Overrides the icon for shouldShowRightIcon */ + iconRight?: IconType; + + /** Should render component on the right */ + shouldShowRightComponent?: boolean; + + /** Component to be displayed on the right */ + rightComponent?: ReactNode; + /** A description text to show under the title */ description?: string; @@ -278,7 +253,7 @@ function MenuItem({ return title ? `${title}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, html]); - const hasPressableRightComponent = iconRight || (rightComponent && shouldShowRightComponent); + const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent); const renderTitleContent = () => { if (titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { @@ -427,7 +402,7 @@ function MenuItem({ {renderTitleContent()} )} - {Boolean(shouldShowTitleIcon) && ( + {Boolean(shouldShowTitleIcon) && Boolean(titleIcon) && ( Date: Tue, 21 Nov 2023 13:00:13 +0100 Subject: [PATCH 042/244] Typing --- src/components/Badge.tsx | 2 +- src/components/DisplayNames/types.ts | 6 +- src/components/Icon/index.tsx | 4 ++ src/components/MenuItem.tsx | 98 +++++++++++++++------------- 4 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 22c056dfdfc4..575646f7dd9c 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -29,7 +29,7 @@ type BadgeProps = { textStyles?: StyleProp; /** Callback to be called on onPress */ - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void; }; function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) { diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 94e4fc7c39c6..1dcaf3e1f9c1 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -29,7 +29,7 @@ type DisplayNamesProps = { tooltipEnabled?: boolean; /** Arbitrary styles of the displayName text */ - textStyles: StyleProp; + textStyles?: StyleProp; /** * Overrides the text that's read by the screen reader when the user interacts with the element. By default, the @@ -42,3 +42,7 @@ type DisplayNamesProps = { }; export default DisplayNamesProps; + +export type { + DisplayNameWithTooltip +} diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 022c740907ea..4b8f27ad1358 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -100,3 +100,7 @@ class Icon extends PureComponent { } export default Icon; + +export type { + SrcProps +} diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 12f9aa36c913..6516056c3906 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,7 +1,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {FC, ForwardedRef, ReactNode, forwardRef, useEffect, useMemo} from 'react'; -import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; -import _ from 'underscore'; +import React, {FC, ForwardedRef, ReactNode, forwardRef, useEffect, useMemo, useRef, useState} from 'react'; +import {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; @@ -21,7 +20,7 @@ import Avatar from './Avatar'; import Badge from './Badge'; import DisplayNames from './DisplayNames'; import Hoverable from './Hoverable'; -import Icon from './Icon'; +import Icon, { SrcProps } from './Icon'; import * as Expensicons from './Icon/Expensicons'; import * as defaultWorkspaceAvatars from './Icon/WorkspaceDefaultAvatars'; import MultipleAvatars from './MultipleAvatars'; @@ -29,6 +28,7 @@ import PressableWithSecondaryInteraction from './PressableWithSecondaryInteracti import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; +import { DisplayNameWithTooltip } from './DisplayNames/types'; type ResponsiveProps = { /** Function to fire when component is pressed */ @@ -60,14 +60,16 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { /** Any adjustments to style when menu item is hovered or pressed */ hoverAndPressStyle: StyleProp>; + descriptionTextStyle?: StyleProp; + /** Icon to display on the left side of component */ icon?: ReactNode | string | AvatarType; - + /** The fill color to pass into the icon. */ iconFill?: string; /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon?: ReactNode; + secondaryIcon?: (props: SrcProps) => React.ReactNode; /** The fill color to pass into the secondary icon. */ secondaryIconFill?: string; @@ -157,7 +159,11 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { floatRightAvatars?: AvatarType[]; /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize?: typeof CONST.AVATAR_SIZE; + floatRightAvatarSize?: typeof CONST.AVATAR_SIZE[keyof typeof CONST.AVATAR_SIZE]; + + viewMode?: typeof CONST.OPTION_MODE[keyof typeof CONST.OPTION_MODE]; + + numberOfLinesTitle?: number; /** Whether we should use small avatar subscript sizing the for menu item */ isSmallAvatarSubscriptMenu?: boolean; @@ -189,10 +195,12 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & { onSecondaryInteraction: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: ReactNode[]; + titleWithTooltips: DisplayNameWithTooltip[]; }; + function MenuItem({ interactive = true, onPress, badgeText, style = styles.popoverMenuItem, wrapperStyle, titleStyle, hoverAndPressStyle, + descriptionTextStyle = styles.breakWord, viewMode = CONST.OPTION_MODE.DEFAULT, numberOfLinesTitle = 1, icon, iconFill, secondaryIcon, secondaryIconFill, iconType = CONST.ICON_TYPE_ICON, iconWidth, iconHeight, iconStyles, fallbackIcon = Expensicons.FallbackAvatar, shouldShowTitleIcon = false, titleIcon, shouldShowRightIcon = false, iconRight = Expensicons.ArrowRight, furtherDetailsIcon, furtherDetails, description, error, success = false, focused = false, disabled = false, @@ -203,34 +211,32 @@ function MenuItem({ shouldBlockSelection = false, shouldParseTitle = false, shouldCheckActionAllowedOnPress = true, onSecondaryInteraction, titleWithTooltips }: MenuItemProps, ref: ForwardedRef) { const {isSmallScreenWidth} = useWindowDimensions(); - const [html, setHtml] = React.useState(''); + const [html, setHtml] = useState(''); + const titleRef = useRef(''); - const isDeleted = _.contains(style, styles.offlineFeedback.deleted); const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; + const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; const titleTextStyle = StyleUtils.combineStyles( [ styles.flexShrink1, styles.popoverMenuText, - icon && !_.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), + icon && !Array.isArray(icon) && (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3), shouldShowBasicTitle ? undefined : styles.textStrong, numberOfLinesTitle !== 1 ? styles.preWrap : styles.pre, interactive && disabled ? {...styles.userSelectNone} : undefined, styles.ltr, - isDeleted ? styles.offlineFeedback.deleted : undefined, + styles.offlineFeedback.deleted ], titleStyle, ); - const descriptionTextStyle = StyleUtils.combineStyles([ + const descriptionTextStyles = StyleUtils.combineStyles([ styles.textLabelSupporting, - icon && !_.isArray(icon) ? styles.ml3 : undefined, + icon && !Array.isArray(icon) && styles.ml3, title ? descriptionVerticalMargin : StyleUtils.getFontSizeStyle(variables.fontSizeNormal), descriptionTextStyle, - isDeleted ? styles.offlineFeedback.deleted : undefined, + styles.offlineFeedback.deleted ]); - const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; - - const titleRef = React.useRef(''); useEffect(() => { if (!title || (titleRef.current.length && titleRef.current === title) || !shouldParseTitle) { return; @@ -241,22 +247,22 @@ function MenuItem({ }, [title, shouldParseTitle]); const getProcessedTitle = useMemo(() => { - let title = ''; + let processedTitle = ''; if (shouldRenderAsHTML) { - title = convertToLTR(title); + processedTitle = title ? convertToLTR(title) : ''; } if (shouldParseTitle) { - title = html; + processedTitle = html; } - return title ? `${title}` : ''; + return processedTitle ? `${processedTitle}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, html]); const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent); const renderTitleContent = () => { - if (titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { + if (title && titleWithTooltips && Array.isArray(titleWithTooltips) && titleWithTooltips.length > 0) { return ( shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={onSecondaryInteraction} - style={({pressed}) => [ + style={({pressed}: { pressed: PressableStateCallbackType }) => [ style, !interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, Boolean(pressed), success, disabled, interactive), true), (isHovered || pressed) && hoverAndPressStyle, - ...(_.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), + ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, ]} disabled={disabled} @@ -308,16 +316,16 @@ function MenuItem({ {Boolean(label) && ( - + {label} )} - {Boolean(icon) && _.isArray(icon) && ( + {Boolean(icon) && Array.isArray(icon) && ( )} - {Boolean(icon) && !_.isArray(icon) && ( + {Boolean(icon) && !Array.isArray(icon) && ( {iconType === CONST.ICON_TYPE_ICON && ( )} - {Boolean(secondaryIcon) && ( + {secondaryIcon && ( @@ -381,7 +389,7 @@ function MenuItem({ {Boolean(description) && shouldShowDescriptionOnTop && ( {description} @@ -413,7 +421,7 @@ function MenuItem({ {Boolean(description) && !shouldShowDescriptionOnTop && ( {description} @@ -444,7 +452,7 @@ function MenuItem({ - {Boolean(badgeText) && ( + {badgeText && ( )} {/* Since subtitle can be of type number, we should allow 0 to be shown */} - {(subtitle || subtitle === 0) && ( + {(subtitle ?? subtitle === 0) && ( - {subtitle} + {subtitle} )} {!_.isEmpty(floatRightAvatars) && ( @@ -489,7 +497,7 @@ function MenuItem({ )} From 800d2b1ab29eb344244d3ce325eca31f7c59305a Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 22 Nov 2023 23:02:57 +0000 Subject: [PATCH 043/244] fix(expensify card page): not found state showing up and disappearing immediately --- .../settings/Wallet/ExpensifyCardPage.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index e92fca171817..beee9d63d984 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -87,7 +87,7 @@ const propTypes = { }; const defaultProps = { - cardList: {}, + cardList: null, draftValues: { addressLine1: '', addressLine2: '', @@ -127,17 +127,21 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const domainCards = CardUtils.getDomainCards(cardList)[domain]; - const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; - const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; + const domainCards = useMemo(() => cardList && CardUtils.getDomainCards(cardList)[domain], [cardList, domain]); + const virtualCard = useMemo(() => (domainCards && _.find(domainCards, (card) => card.isVirtual)) || {}, [domainCards]); + const physicalCard = useMemo(() => (domainCards && _.find(domainCards, (card) => !card.isVirtual)) || {}, [domainCards]); const [isLoading, setIsLoading] = useState(false); + const [isNotFound, setIsNotFound] = useState(false); const [details, setDetails] = useState({}); const [cardDetailsError, setCardDetailsError] = useState(''); - if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { - return Navigation.goBack(ROUTES.SETTINGS_WALLET)} />; - } + useEffect(() => { + if (!cardList) { + return; + } + setIsNotFound(_.isEmpty(virtualCard) && _.isEmpty(physicalCard)); + }, [cardList, physicalCard, virtualCard]); const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); @@ -166,6 +170,10 @@ function ExpensifyCardPage({ const hasDetectedIndividualFraud = _.some(domainCards, (card) => card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); const cardDetailsErrorObject = cardDetailsError ? {error: cardDetailsError} : {}; + if (isNotFound) { + return Navigation.goBack(ROUTES.SETTINGS_WALLET)} />; + } + return ( Date: Thu, 23 Nov 2023 12:19:13 +0000 Subject: [PATCH 044/244] fix(get physical card): weird back button behaviour --- src/libs/GetPhysicalCardUtils.ts | 2 +- src/libs/Navigation/linkTo.js | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 57a9d773cc9d..80391108e377 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -82,7 +82,7 @@ function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDe } // Redirect the user if he's not allowed to be on the current step - Navigation.navigate(expectedRoute, CONST.NAVIGATION.ACTION_TYPE.REPLACE); + Navigation.goBack(expectedRoute); } /** diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.js index ca87a0d7b79c..db8b0493272c 100644 --- a/src/libs/Navigation/linkTo.js +++ b/src/libs/Navigation/linkTo.js @@ -89,11 +89,6 @@ export default function linkTo(navigation, path, type, isActiveRoute) { if (!isActiveRoute && type === CONST.NAVIGATION.ACTION_TYPE.PUSH) { minimalAction.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; } - // There are situations when the user is trying to access a route which he has no access to - // So we want to redirect him to the right one and replace the one he tried to access - if (type === CONST.NAVIGATION.ACTION_TYPE.REPLACE) { - minimalAction.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; - } root.dispatch(minimalAction); return; } From 8d14ca8003d9bf58639f57e7071fc0a31bacba39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Mon, 27 Nov 2023 23:02:00 +0100 Subject: [PATCH 045/244] refactor theme to functional way --- src/pages/SearchPage.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 0fdd54da74f9..05405fce3cec 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -8,7 +8,7 @@ import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; +import useThemeStyles from "@styles/useThemeStyles"; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; @@ -34,7 +34,6 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, - ...withThemeStylesPropTypes, }; const defaultProps = { @@ -63,6 +62,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); + const themeStyles = useThemeStyles(); const isMounted = useRef(false); const updateOptions = useCallback(() => { @@ -177,7 +177,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( <> - + Date: Fri, 24 Nov 2023 17:03:55 +0100 Subject: [PATCH 046/244] add FormProvider in page --- src/pages/workspace/WorkspaceInviteMessagePage.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 85dd65377dcf..739f7bf3695a 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -27,6 +27,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; const personalDetailsPropTypes = PropTypes.shape({ /** The accountID of the person */ @@ -178,7 +180,7 @@ class WorkspaceInviteMessagePage extends React.Component { onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(this.props.route.params.policyID))} /> -
{this.props.translate('workspace.inviteMessage.inviteMessagePrompt')} - (this.welcomeMessageInputRef = el)} role={CONST.ACCESSIBILITY_ROLE.TEXT} inputID="welcomeMessage" @@ -233,7 +236,7 @@ class WorkspaceInviteMessagePage extends React.Component { shouldSaveDraft /> -
+
); From 4f479e2f4116b114f79c1a3c82d1073d1c509c70 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:04:30 +0100 Subject: [PATCH 047/244] fix prettier --- src/pages/workspace/WorkspaceInviteMessagePage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 739f7bf3695a..273bcba6e6c2 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -6,6 +6,8 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -27,8 +29,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; const personalDetailsPropTypes = PropTypes.shape({ /** The accountID of the person */ From 148798661d3f72556e3134583245ba8d951e6965 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:05:17 +0100 Subject: [PATCH 048/244] remove unused imports --- src/pages/workspace/WorkspaceInviteMessagePage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 273bcba6e6c2..825c130eeaac 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -5,7 +5,6 @@ import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import Form from '@components/Form'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; From ee5369e3e423eb813835ad84c001cb18ebec6bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fija=C5=82kiewicz?= Date: Tue, 28 Nov 2023 21:30:59 +0100 Subject: [PATCH 049/244] prettier --- src/pages/SearchPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 05405fce3cec..b0eb45876520 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -8,11 +8,11 @@ import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useThemeStyles from "@styles/useThemeStyles"; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; +import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; From b350c6b9fcfde9eb5aa68cea5ba0d881bce6ec7c Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 00:36:24 +0200 Subject: [PATCH 050/244] Update src/libs/ModifiedExpenseMessage.ts Co-authored-by: Tim Golen --- src/libs/ModifiedExpenseMessage.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 44259180f4eb..544ced26cc28 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -72,15 +72,6 @@ function getMessageLine(prefix: string, messageFragments: string[]) { }, prefix); } -/** - * Get the message for a modified distance request. - * - * @param newDistance - * @param oldDistance - * @param newAmount - * @param oldAmount - */ - function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string) { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); From 03931a24e14d0746eada51c63a4a3f630fff5c75 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 00:36:33 +0200 Subject: [PATCH 051/244] Update src/libs/ModifiedExpenseMessage.ts Co-authored-by: Tim Golen --- src/libs/ModifiedExpenseMessage.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 544ced26cc28..e82c037cf632 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -89,8 +89,6 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. - * - * @param reportAction */ function getForReportAction(reportAction: ReportAction) { if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { From 4e9a118da27b1233123d9c5552925d5f71475b5f Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 00:36:41 +0200 Subject: [PATCH 052/244] Update src/libs/ModifiedExpenseMessage.ts Co-authored-by: Tim Golen --- src/libs/ModifiedExpenseMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index e82c037cf632..8e7b27b55ce4 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -90,7 +90,7 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. */ -function getForReportAction(reportAction: ReportAction) { +function getForReportAction(reportAction: ReportAction): string { if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { return ''; } From ad9ba61167e344cf3123b763d2a6c72c3b40d7dc Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 29 Nov 2023 13:35:45 +0700 Subject: [PATCH 053/244] fix: room name in invite message does not show link --- src/libs/ReportUtils.ts | 9 +++++-- .../report/ContextMenu/ContextMenuActions.js | 24 ++++++++++++------- src/types/onyx/OriginalMessage.ts | 1 + 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d93661778b83..8ea82d91e0bf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -23,6 +23,7 @@ import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; +import * as Environment from './Environment/Environment'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -346,6 +347,9 @@ type OnyxDataTaskAssigneeChat = { optimisticChatCreatedReportAction?: OptimisticCreatedReportAction; }; +let environmentURL: string; +Environment.getEnvironmentURL().then((url: string) => (environmentURL = url)); + let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -4164,13 +4168,14 @@ function getChannelLogMemberMessage(reportAction: OnyxEntry): stri message = `${verb} ${mentions?.join(', ')}, and ${lastMention}`; } + const reportID = (reportAction?.originalMessage as ChangeLog)?.reportID ?? 0; const roomName = (reportAction?.originalMessage as ChangeLog)?.roomName ?? ''; - if (roomName) { + if (reportID && roomName) { const preposition = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ? ' to' : ' from'; - message += `${preposition} ${roomName}`; + message += `${preposition} ${roomName}`; } return message; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 4f35926c5957..973971c60f72 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -23,6 +23,20 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; +/** + * Sets the HTML string to Clipboard. + * @param {String} content + */ +function setClipboardHtmlMessage(content) { + const parser = new ExpensiMark(); + if (!Clipboard.canSetHtml()) { + Clipboard.setString(parser.htmlToMarkdown(content)); + } else { + const plainText = parser.htmlToText(content); + Clipboard.setHtml(content, plainText); + } +} + /** * Gets the HTML version of the message in an action. * @param {Object} reportAction @@ -283,15 +297,9 @@ export default [ Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isChannelLogMemberAction(reportAction)) { const logMessage = ReportUtils.getChannelLogMemberMessage(reportAction); - Clipboard.setString(logMessage); + setClipboardHtmlMessage(logMessage); } else if (content) { - const parser = new ExpensiMark(); - if (!Clipboard.canSetHtml()) { - Clipboard.setString(parser.htmlToMarkdown(content)); - } else { - const plainText = parser.htmlToText(content); - Clipboard.setHtml(content, plainText); - } + setClipboardHtmlMessage(content); } } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 0dc532ebeded..b00dee0df96a 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -140,6 +140,7 @@ type ChronosOOOTimestamp = { type ChangeLog = { targetAccountIDs?: number[]; roomName?: string; + reportID?: number; }; type ChronosOOOEvent = { From ebcd66fd9fe5ccb98c384281342e2e162bf29b20 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 29 Nov 2023 13:50:25 +0700 Subject: [PATCH 054/244] remove redundat escape charaters --- 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 8ea82d91e0bf..728cf44d7d9f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4175,7 +4175,7 @@ function getChannelLogMemberMessage(reportAction: OnyxEntry): stri reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ? ' to' : ' from'; - message += `${preposition} ${roomName}`; + message += `${preposition} ${roomName}`; } return message; From fd0a9a9eb6325ba8e0643b3d250217831244d336 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 29 Nov 2023 13:57:06 +0700 Subject: [PATCH 055/244] fix: mention in copied invite message shown as hidden --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 728cf44d7d9f..49af0c71c8fb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4151,8 +4151,8 @@ function getChannelLogMemberMessage(reportAction: OnyxEntry): stri ? 'invited' : 'removed'; - const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map(() => { - const personalDetail = allPersonalDetails?.accountID; + const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map((accountID) => { + const personalDetail = allPersonalDetails?.[accountID]; const displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || (personalDetail?.displayName ?? '') || Localize.translateLocal('common.hidden'); return `@${displayNameOrLogin}`; }); From 41f8a94198a6eb74f4662e1edd4f17b6fe46c88f Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 17:26:37 +0200 Subject: [PATCH 056/244] Resolve conflicts after merge. --- src/libs/ReportUtils.ts | 143 ++------------------------------- src/types/onyx/ReportAction.ts | 1 - 2 files changed, 8 insertions(+), 136 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d93661778b83..da00bc1c5c18 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1953,140 +1953,6 @@ function getReportPreviewMessage( return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); } -/** - * Get the proper message schema for modified expense message. - */ - -function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true): string { - const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; - const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; - const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; - - if (!oldValue) { - return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); - } - if (!newValue) { - return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); - } - return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); -} - -/** - * Get the proper message schema for modified distance message. - */ -function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string): string { - if (!oldDistance) { - return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); - } - return Localize.translateLocal('iou.updatedTheDistance', { - newDistanceToDisplay: newDistance, - oldDistanceToDisplay: oldDistance, - newAmountToDisplay: newAmount, - oldAmountToDisplay: oldAmount, - }); -} - -/** - * Get the report action message when expense has been modified. - * - * ModifiedExpense::getNewDotComment in Web-Expensify should match this. - * If we change this function be sure to update the backend as well. - */ -function getModifiedExpenseMessage(reportAction: OnyxEntry): string | undefined { - const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage | undefined; - if (isEmptyObject(reportActionOriginalMessage)) { - return Localize.translateLocal('iou.changedTheRequest'); - } - const reportID = reportAction?.reportID ?? ''; - const policyID = getReport(reportID)?.policyID ?? ''; - const policyTags = getPolicyTags(policyID); - const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = policyTag?.name ?? Localize.translateLocal('common.tag'); - - const hasModifiedAmount = - reportActionOriginalMessage && - 'oldAmount' in reportActionOriginalMessage && - 'oldCurrency' in reportActionOriginalMessage && - 'amount' in reportActionOriginalMessage && - 'currency' in reportActionOriginalMessage; - - const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; - if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage?.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency ?? ''); - - const currency = reportActionOriginalMessage?.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency); - - // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. - // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage?.merchant?.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); - } - - return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); - } - - const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; - if (hasModifiedComment) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.newComment ?? '', - reportActionOriginalMessage?.oldComment ?? '', - Localize.translateLocal('common.description'), - true, - ); - } - - const hasModifiedCreated = reportActionOriginalMessage && 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; - if (hasModifiedCreated) { - // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated: Date | string = new Date(reportActionOriginalMessage?.oldCreated ?? 0); - formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created ?? '', formattedOldCreated?.toString?.(), Localize.translateLocal('common.date'), false); - } - - if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.merchant ?? '', - reportActionOriginalMessage?.oldMerchant ?? '', - Localize.translateLocal('common.merchant'), - true, - ); - } - - const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; - if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.category ?? '', - reportActionOriginalMessage?.oldCategory ?? '', - Localize.translateLocal('common.category'), - true, - ); - } - - const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; - if (hasModifiedTag) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.tag ?? '', - reportActionOriginalMessage.oldTag ?? '', - policyTagListName, - true, - policyTagListName === Localize.translateLocal('common.tag'), - ); - } - - const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; - if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.billable ?? '', - reportActionOriginalMessage?.oldBillable ?? '', - Localize.translateLocal('iou.request'), - true, - ); - } -} - /** * Given the updates user made to the request, compose the originalMessage * object of the modified expense action. @@ -3650,6 +3516,13 @@ function getReportIDFromLink(url: string | null): string { return reportID; } +/** + * Get the report policyID given a reportID + */ +function getReportPolicyID(reportID: string | undefined): string|undefined { + return getReport(reportID)?.policyID; +} + /** * Check if the chat report is linked to an iou that is waiting for the current user to add a credit bank account. */ @@ -4267,6 +4140,7 @@ export { getReport, getReportNotificationPreference, getReportIDFromLink, + getReportPolicyID, getRouteFromLink, getDeletedParentActionMessageForChatReport, getLastVisibleMessage, @@ -4342,7 +4216,6 @@ export { getParentReport, getRootParentReport, getReportPreviewMessage, - getModifiedExpenseMessage, canUserPerformWriteAction, getOriginalReportID, canAccessReport, diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 95f5574e5432..891a0ffcb7b8 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -90,7 +90,6 @@ type ReportActionBase = { automatic?: boolean; shouldShow?: boolean; - reportID?: string; /** The ID of childReport */ childReportID?: string; From ceff5c197b0301bd3dd95e0e5cd45389b76d9cb1 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 17:36:56 +0200 Subject: [PATCH 057/244] Move policyTags to ModifiedExpenseMessage --- src/libs/ModifiedExpenseMessage.ts | 21 ++++++++++++++++++--- src/libs/PolicyUtils.ts | 19 +------------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 8e7b27b55ce4..decc4c5a9863 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,11 +1,26 @@ import {format} from 'date-fns'; import CONST from '@src/CONST'; -import {ReportAction} from '@src/types/onyx'; +import {PolicyTags, ReportAction} from '@src/types/onyx'; +import Onyx, {OnyxCollection} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; + +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + return; + } + allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); + }, +}); + /** * Builds the partial message fragment for a modified field on the expense. * @@ -95,8 +110,8 @@ function getForReportAction(reportAction: ReportAction): string { return ''; } const reportActionOriginalMessage = reportAction.originalMessage; - const policyID = ReportUtils.getReportPolicyID(reportAction.reportID ?? ''); - const policyTags = PolicyUtils.getPolicyTags(policyID); + const policyID = ReportUtils.getReportPolicyID(reportAction.reportID) ?? ''; + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); const removalFragments: string[] = []; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index e7328972cfa1..19129959d016 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; @@ -9,22 +9,6 @@ type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; type UnitRate = {rate: number}; -let allPolicyTags: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - return; - } - allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); - }, -}); - -function getPolicyTags(policyID: string) { - return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; -} - /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. @@ -216,7 +200,6 @@ function isPendingDeletePolicy(policy: OnyxEntry): boolean { export { getActivePolicies, - getPolicyTags, hasPolicyMemberError, hasPolicyError, hasPolicyErrorFields, From 57f083782f1e8ebbfc36ead4f7d0f49854262e99 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 17:55:30 +0200 Subject: [PATCH 058/244] Remove unused function. --- src/libs/ModifiedExpenseMessage.ts | 7 ++++--- src/libs/ReportUtils.ts | 19 ------------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index decc4c5a9863..293411972858 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,7 +1,7 @@ import {format} from 'date-fns'; import CONST from '@src/CONST'; import {PolicyTags, ReportAction} from '@src/types/onyx'; -import Onyx, {OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; @@ -9,15 +9,16 @@ import * as PolicyUtils from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; -let allPolicyTags: OnyxCollection = {}; +let allPolicyTags: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, callback: (value) => { if (!value) { + allPolicyTags = {}; return; } - allPolicyTags = Object.fromEntries(Object.entries(value).filter(([, policyTags]) => !!policyTags)); + allPolicyTags = value; }, }); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index da00bc1c5c18..2280f3283b80 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -400,25 +400,6 @@ Onyx.connect({ callback: (value) => (loginList = value), }); -let allPolicyTags: Record = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - -function getPolicyTags(policyID: string) { - return allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; -} - function getChatType(report: OnyxEntry): ValueOf | undefined { return report?.chatType; } From 7dd7afb9265cc2d10eaab94313b72520749738b0 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 18:25:48 +0200 Subject: [PATCH 059/244] Make lint happy. --- src/libs/ModifiedExpenseMessage.ts | 5 ++--- src/libs/ReportUtils.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 293411972858..558409fe3a51 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,14 +1,13 @@ import {format} from 'date-fns'; -import CONST from '@src/CONST'; -import {PolicyTags, ReportAction} from '@src/types/onyx'; import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {PolicyTags, ReportAction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; import * as Localize from './Localize'; import * as PolicyUtils from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; - let allPolicyTags: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2280f3283b80..dabfa271f7c7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,4 +1,3 @@ -import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashEscape from 'lodash/escape'; @@ -14,7 +13,7 @@ import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; +import {Beta, Login, PersonalDetails, Policy, Report, ReportAction, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActions} from '@src/types/onyx/ReportAction'; @@ -3500,7 +3499,7 @@ function getReportIDFromLink(url: string | null): string { /** * Get the report policyID given a reportID */ -function getReportPolicyID(reportID: string | undefined): string|undefined { +function getReportPolicyID(reportID: string | undefined): string | undefined { return getReport(reportID)?.policyID; } From 1cb7f4751f3b99d9e462a871e2a33d821c50c761 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Wed, 29 Nov 2023 18:33:51 +0200 Subject: [PATCH 060/244] Better checks. --- src/libs/ModifiedExpenseMessage.ts | 57 ++++++++++++++++-------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 558409fe3a51..aaa6cd10f656 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -118,30 +118,35 @@ function getForReportAction(reportAction: ReportAction): string { const setFragments: string[] = []; const changeFragments: string[] = []; - const hasModifiedAmount = reportActionOriginalMessage.oldAmount && reportActionOriginalMessage.oldCurrency && reportActionOriginalMessage.amount && reportActionOriginalMessage.currency; - - const hasModifiedMerchant = reportActionOriginalMessage.oldMerchant && reportActionOriginalMessage.merchant; + const hasModifiedAmount = + reportActionOriginalMessage && + 'oldAmount' in reportActionOriginalMessage && + 'oldCurrency' in reportActionOriginalMessage && + 'amount' in reportActionOriginalMessage && + 'currency' in reportActionOriginalMessage; + + const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage.oldCurrency ?? ''; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount ?? 0, oldCurrency); + const oldCurrency = reportActionOriginalMessage?.oldCurrency ?? ''; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency); - const currency = reportActionOriginalMessage.currency ?? ''; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount ?? 0, currency); + const currency = reportActionOriginalMessage?.currency ?? ''; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency); // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && (reportActionOriginalMessage.merchant ?? '').includes('@')) { - return getForDistanceRequest(reportActionOriginalMessage.merchant ?? '', reportActionOriginalMessage.oldMerchant ?? '', amount, oldAmount); + if (hasModifiedMerchant && (reportActionOriginalMessage?.merchant ?? '').includes('@')) { + return getForDistanceRequest(reportActionOriginalMessage?.merchant ?? '', reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); } buildMessageFragmentForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false, setFragments, removalFragments, changeFragments); } - const hasModifiedComment = reportActionOriginalMessage.oldComment && reportActionOriginalMessage.newComment; + const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; if (hasModifiedComment) { buildMessageFragmentForValue( - reportActionOriginalMessage.newComment ?? '', - reportActionOriginalMessage.oldComment ?? '', + reportActionOriginalMessage?.newComment ?? '', + reportActionOriginalMessage?.oldComment ?? '', Localize.translateLocal('common.description'), true, setFragments, @@ -150,12 +155,12 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedCreated = reportActionOriginalMessage.oldCreated && reportActionOriginalMessage.created; + const hasModifiedCreated = reportActionOriginalMessage && 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - const formattedOldCreated = format(new Date(reportActionOriginalMessage.oldCreated ?? ''), CONST.DATE.FNS_FORMAT_STRING); + const formattedOldCreated = format(new Date(reportActionOriginalMessage?.oldCreated ?? ''), CONST.DATE.FNS_FORMAT_STRING); buildMessageFragmentForValue( - reportActionOriginalMessage.created ?? '', + reportActionOriginalMessage?.created ?? '', formattedOldCreated, Localize.translateLocal('common.date'), false, @@ -167,8 +172,8 @@ function getForReportAction(reportAction: ReportAction): string { if (hasModifiedMerchant) { buildMessageFragmentForValue( - reportActionOriginalMessage.merchant ?? '', - reportActionOriginalMessage.oldMerchant ?? '', + reportActionOriginalMessage?.merchant ?? '', + reportActionOriginalMessage?.oldMerchant ?? '', Localize.translateLocal('common.merchant'), true, setFragments, @@ -177,11 +182,11 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedCategory = reportActionOriginalMessage.oldCategory && reportActionOriginalMessage.category; + const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; if (hasModifiedCategory) { buildMessageFragmentForValue( - reportActionOriginalMessage.category ?? '', - reportActionOriginalMessage.oldCategory ?? '', + reportActionOriginalMessage?.category ?? '', + reportActionOriginalMessage?.oldCategory ?? '', Localize.translateLocal('common.category'), true, setFragments, @@ -190,11 +195,11 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedTag = reportActionOriginalMessage.oldTag && reportActionOriginalMessage.tag; + const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; if (hasModifiedTag) { buildMessageFragmentForValue( - reportActionOriginalMessage.tag ?? '', - reportActionOriginalMessage.oldTag ?? '', + reportActionOriginalMessage?.tag ?? '', + reportActionOriginalMessage?.oldTag ?? '', policyTagListName, true, setFragments, @@ -204,11 +209,11 @@ function getForReportAction(reportAction: ReportAction): string { ); } - const hasModifiedBillable = reportActionOriginalMessage.oldBillable && reportActionOriginalMessage.billable; + const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; if (hasModifiedBillable) { buildMessageFragmentForValue( - reportActionOriginalMessage.billable ?? '', - reportActionOriginalMessage.oldBillable ?? '', + reportActionOriginalMessage?.billable ?? '', + reportActionOriginalMessage?.oldBillable ?? '', Localize.translateLocal('iou.request'), true, setFragments, From 4bd79291e06168e6907590e10f4f00d425e33a0a Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 29 Nov 2023 16:52:04 +0000 Subject: [PATCH 061/244] refactor(typescript): migrate IFrame --- src/components/{IFrame.js => IFrame.tsx} | 35 ++++++++++-------------- 1 file changed, 15 insertions(+), 20 deletions(-) rename src/components/{IFrame.js => IFrame.tsx} (82%) diff --git a/src/components/IFrame.js b/src/components/IFrame.tsx similarity index 82% rename from src/components/IFrame.js rename to src/components/IFrame.tsx index aa85ad03ffbf..eca9524b1cb1 100644 --- a/src/components/IFrame.js +++ b/src/components/IFrame.tsx @@ -1,10 +1,9 @@ -/* eslint-disable es/no-nullish-coalescing-operators */ -import PropTypes from 'prop-types'; import React, {useEffect, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Session} from '@src/types/onyx'; -function getNewDotURL(url) { +function getNewDotURL(url: string | URL) { const urlObj = new URL(url); const paramString = urlObj.searchParams.get('param') ?? ''; const pathname = urlObj.pathname.slice(1); @@ -48,7 +47,7 @@ function getNewDotURL(url) { return pathname; } -function getOldDotURL(url) { +function getOldDotURL(url: string | URL) { const urlObj = new URL(url); const pathname = urlObj.pathname; const paths = pathname.slice(1).split('/'); @@ -86,18 +85,13 @@ function getOldDotURL(url) { return pathname; } -const propTypes = { - // The session of the logged in person - session: PropTypes.shape({ - // The email of the logged in person - email: PropTypes.string, - - // The authToken of the logged in person - authToken: PropTypes.string, - }).isRequired, +type OldDotIFrameOnyxProps = { + session: OnyxEntry; }; -function OldDotIFrame({session}) { +type OldDotIFrameProps = OldDotIFrameOnyxProps; + +function OldDotIFrame({session}: OldDotIFrameProps) { const [oldDotURL, setOldDotURL] = useState('https://staging.expensify.com'); useEffect(() => { @@ -106,15 +100,18 @@ function OldDotIFrame({session}) { window.addEventListener('message', (event) => { const url = event.data; // TODO: use this value to navigate to a new path - // eslint-disable-next-line no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars const newDotURL = getNewDotURL(url); }); }, []); useEffect(() => { + if (!session) { + return; + } document.cookie = `authToken=${session.authToken}; domain=expensify.com.dev; path=/;`; document.cookie = `email=${session.email}; domain=expensify.com.dev; path=/;`; - }, [session.authToken, session.email]); + }, [session]); return (