From 6749e0abc0d041cfe0c4349c36a5982045cf1ae6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 7 Dec 2023 22:39:15 +0700 Subject: [PATCH 001/297] fix: pdf not cached --- src/components/PDFView/index.native.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index b022823d215a..417106618c6d 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -11,6 +11,7 @@ import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeSt import withWindowDimensions from '@components/withWindowDimensions'; import compose from '@libs/compose'; import * as StyleUtils from '@styles/StyleUtils'; +import * as CachedPDFPaths from '@userActions/CachedPDFPaths'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; @@ -118,7 +119,9 @@ class PDFView extends Component { * After the PDF is successfully loaded hide PDFPasswordForm and the loading * indicator. */ - finishPDFLoad() { + finishPDFLoad(_, path) { + console.log(path) + // CachedPDFPaths.save(path); this.setState({ shouldRequestPassword: false, shouldShowLoadingIndicator: false, @@ -155,7 +158,7 @@ class PDFView extends Component { fitPolicy={0} trustAllCerts={false} renderActivityIndicator={() => } - source={{uri: this.props.sourceURL}} + source={{uri: this.props.sourceURL, cache: true, expiration: 864000}} style={pdfStyles} onError={this.handleFailureToLoadPDF} password={this.state.password} From 0624e89a3f80065d08f2c271e78b505f779520e1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 7 Dec 2023 22:40:08 +0700 Subject: [PATCH 002/297] fix lint --- src/components/PDFView/index.native.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 417106618c6d..4f09906ad6fe 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -11,7 +11,6 @@ import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeSt import withWindowDimensions from '@components/withWindowDimensions'; import compose from '@libs/compose'; import * as StyleUtils from '@styles/StyleUtils'; -import * as CachedPDFPaths from '@userActions/CachedPDFPaths'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; @@ -120,8 +119,6 @@ class PDFView extends Component { * indicator. */ finishPDFLoad(_, path) { - console.log(path) - // CachedPDFPaths.save(path); this.setState({ shouldRequestPassword: false, shouldShowLoadingIndicator: false, From 552592cd2ab1b75e54f10e4450dec427fe6dee02 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 7 Dec 2023 22:56:24 +0700 Subject: [PATCH 003/297] fix lint --- src/components/PDFView/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 4f09906ad6fe..69a47ce82d9c 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -118,7 +118,7 @@ class PDFView extends Component { * After the PDF is successfully loaded hide PDFPasswordForm and the loading * indicator. */ - finishPDFLoad(_, path) { + finishPDFLoad() { this.setState({ shouldRequestPassword: false, shouldShowLoadingIndicator: false, From 2a4d712ab8651d58ba1c2e12c63d59a1e9178c54 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 22 Dec 2023 16:08:45 +0800 Subject: [PATCH 004/297] update ioutype params when selecting a split participant --- src/libs/actions/IOU.js | 15 ++++++++++++ ...yForRefactorRequestParticipantsSelector.js | 23 ++++++++++-------- .../step/IOURequestStepParticipants.js | 24 ++++++++++++++++--- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index a9d04654f8bd..1c8f30d75498 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -23,6 +23,7 @@ import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import * as Policy from './Policy'; import * as Report from './Report'; @@ -169,6 +170,19 @@ function clearMoneyRequest(transactionID) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, null); } +/** + * @param {Object[]} routes + * @param {String} newIouType + */ +function updateMoneyRequestTypeParams(routes, newIouType) { + routes.forEach((route) => { + if (route.name !== SCREENS.MONEY_REQUEST.CREATE && route.name !== SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS) { + return; + } + Navigation.setParams({iouType: newIouType}, route.key); + }); +} + /** * @param {String} transactionID * @param {Number} amount @@ -3481,6 +3495,7 @@ export { resetMoneyRequestTag, resetMoneyRequestTag_temporaryForRefactor, clearMoneyRequest, + updateMoneyRequestTypeParams, setMoneyRequestAmount_temporaryForRefactor, setMoneyRequestBillable_temporaryForRefactor, setMoneyRequestCategory_temporaryForRefactor, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 8d7d5cfceb77..6952f2e3cfa4 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -163,13 +163,16 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ * @param {Object} option */ const addSingleParticipant = (option) => { - onParticipantsAdded([ - { - ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), - selected: true, - }, - ]); - onFinish(); + onParticipantsAdded( + [ + { + ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), + selected: true, + }, + ], + false, + ); + onFinish(false); }; /** @@ -208,7 +211,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ ]; } - onParticipantsAdded(newSelectedOptions); + onParticipantsAdded(newSelectedOptions, newSelectedOptions.length !== 0); }, [participants, onParticipantsAdded], ); @@ -276,7 +279,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ return; } - onFinish(); + onFinish(true); }, [shouldShowSplitBillErrorMessage, onFinish]); const footerContent = ( @@ -315,7 +318,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ boldStyle shouldShowConfirmButton={shouldShowSplitBillErrorMessage && isAllowedToSplit} confirmButtonText={translate('iou.addToSplit')} - onConfirmSelection={onFinish} + onConfirmSelection={() => onFinish(true)} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index ec670b828146..38ca52730512 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -1,3 +1,4 @@ +import {useNavigationState} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef} from 'react'; import transactionPropTypes from '@components/transactionPropTypes'; @@ -36,13 +37,16 @@ function IOURequestStepParticipants({ transaction: {participants = []}, }) { const {translate} = useLocalize(); + const routes = useNavigationState((state) => state.routes); const optionsSelectorRef = useRef(); const selectedReportID = useRef(reportID); const numberOfParticipants = useRef(participants.length); const iouRequestType = TransactionUtils.getRequestType(transaction); - const headerTitle = translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT; + const headerTitle = isSplitRequest ? translate('iou.split') : translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); + const newIouType = useRef(null); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then @@ -51,8 +55,22 @@ function IOURequestStepParticipants({ IOUUtils.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID); }, [receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]); + useEffect(() => { + if (!newIouType.current) { + return; + } + IOU.updateMoneyRequestTypeParams(routes, newIouType.current); + newIouType.current = null; + }, [routes, participants]); + const addParticipant = useCallback( - (val) => { + (val, isSplit) => { + if (isSplit && iouType !== CONST.IOU.TYPE.SPLIT) { + newIouType.current = CONST.IOU.TYPE.SPLIT; + } else if (!isSplit && iouType === CONST.IOU.TYPE.SPLIT) { + newIouType.current = CONST.IOU.TYPE.REQUEST; + } + IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); numberOfParticipants.current = val.length; @@ -66,7 +84,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = lodashGet(val, '[0].reportID', reportID); }, - [reportID, transactionID], + [reportID, transactionID, iouType], ); const goToNextStep = useCallback(() => { From 35d1ad8fb9e69da6aa5af7e459fd2a00a9aa02da Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 22 Dec 2023 16:09:39 +0800 Subject: [PATCH 005/297] allow to split with 1 person --- .../request/step/IOURequestStepParticipants.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 38ca52730512..fa9887635d37 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -87,12 +87,15 @@ function IOURequestStepParticipants({ [reportID, transactionID, iouType], ); - const goToNextStep = useCallback(() => { - const nextStepIOUType = numberOfParticipants.current === 1 ? iouType : CONST.IOU.TYPE.SPLIT; - IOU.resetMoneyRequestTag_temporaryForRefactor(transactionID); - IOU.resetMoneyRequestCategory_temporaryForRefactor(transactionID); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(nextStepIOUType, transactionID, selectedReportID.current || reportID)); - }, [iouType, transactionID, reportID]); + const goToNextStep = useCallback( + (isSplit) => { + const nextStepIOUType = !isSplit && iouType !== CONST.IOU.TYPE.REQUEST ? CONST.IOU.TYPE.REQUEST : iouType; + IOU.resetMoneyRequestTag_temporaryForRefactor(transactionID); + IOU.resetMoneyRequestCategory_temporaryForRefactor(transactionID); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(nextStepIOUType, transactionID, selectedReportID.current || reportID)); + }, + [iouType, transactionID, reportID], + ); const navigateBack = useCallback(() => { IOUUtils.navigateToStartMoneyRequestStep(iouRequestType, iouType, transactionID, reportID); From 91c0b691ec326d8115a97fce1d0069b982b1210f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 22 Dec 2023 16:09:57 +0800 Subject: [PATCH 006/297] don't include selected participant if not split --- src/pages/iou/request/step/IOURequestStepParticipants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index fa9887635d37..17c0fc4e4918 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -112,7 +112,7 @@ function IOURequestStepParticipants({ > (optionsSelectorRef.current = el)} - participants={participants} + participants={isSplitRequest ? participants : []} onParticipantsAdded={addParticipant} onFinish={goToNextStep} iouType={iouType} From c4232f34d6e16d037ad8ffb574aa9be29c76dd9e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 22 Dec 2023 16:10:12 +0800 Subject: [PATCH 007/297] reset ioutype if tab changed --- src/pages/iou/request/IOURequestStartPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 6572e154ee14..b5c2348cf72c 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -1,3 +1,4 @@ +import {useNavigationState} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; @@ -61,6 +62,7 @@ function IOURequestStartPage({ }) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const routes = useNavigationState((state) => state.routes); const [isDraggingOver, setIsDraggingOver] = useState(false); const tabTitles = { [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), @@ -103,10 +105,13 @@ function IOURequestStartPage({ if (newIouType === previousIOURequestType) { return; } + if (iouType === CONST.IOU.TYPE.SPLIT) { + IOU.updateMoneyRequestTypeParams(routes, CONST.IOU.TYPE.REQUEST); + } IOU.startMoneyRequest_temporaryForRefactor(reportID, isFromGlobalCreate, newIouType); transactionRequestType.current = newIouType; }, - [previousIOURequestType, reportID, isFromGlobalCreate], + [previousIOURequestType, reportID, isFromGlobalCreate, iouType, routes], ); if (!transaction.transactionID) { From b34854f35fdce47cb9ec05e944eae232e4c91d9e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 1 Jan 2024 22:31:01 +0800 Subject: [PATCH 008/297] add more comment --- src/pages/iou/request/step/IOURequestStepParticipants.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 17c0fc4e4918..8907cb4945e9 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -65,9 +65,13 @@ function IOURequestStepParticipants({ const addParticipant = useCallback( (val, isSplit) => { + // It's only possible to switch between REQUEST and SPLIT. + // We want to update the IOU type only if it's not updated yet to prevent unnecessary updates. if (isSplit && iouType !== CONST.IOU.TYPE.SPLIT) { newIouType.current = CONST.IOU.TYPE.SPLIT; } else if (!isSplit && iouType === CONST.IOU.TYPE.SPLIT) { + // Non-split can be either REQUEST or SEND. Instead of checking whether + // the current IOU type is not a REQUEST (true for SEND), we check whether the current IOU type is a SPLIT. newIouType.current = CONST.IOU.TYPE.REQUEST; } From 756c002b276c7e9d3662e6a3786ed02079166326 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 1 Jan 2024 22:44:38 +0800 Subject: [PATCH 009/297] prettier --- src/pages/iou/request/step/IOURequestStepParticipants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 8907cb4945e9..865695b9e38a 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -70,7 +70,7 @@ function IOURequestStepParticipants({ if (isSplit && iouType !== CONST.IOU.TYPE.SPLIT) { newIouType.current = CONST.IOU.TYPE.SPLIT; } else if (!isSplit && iouType === CONST.IOU.TYPE.SPLIT) { - // Non-split can be either REQUEST or SEND. Instead of checking whether + // Non-split can be either REQUEST or SEND. Instead of checking whether // the current IOU type is not a REQUEST (true for SEND), we check whether the current IOU type is a SPLIT. newIouType.current = CONST.IOU.TYPE.REQUEST; } From 15fb5de877950cd62ef5937bc0dd922cfb44c02c Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 2 Jan 2024 18:59:26 +0800 Subject: [PATCH 010/297] remove unnecessary logic --- src/pages/iou/request/step/IOURequestStepConfirmation.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index cb6225b641fc..0ba57ba012d4 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -104,11 +104,6 @@ function IOURequestStepConfirmation({ // If there is not a report attached to the IOU with a reportID, then the participants were manually selected and the user needs taken // back to the participants step if (!transaction.participantsAutoAssigned) { - // When going back to the participants step, if the iou is a "request" (not a split), then the participants need to be cleared from the - // transaction so that the participant can be selected again. - if (iouType === CONST.IOU.TYPE.REQUEST) { - IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, []); - } Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } From 9f93e082ee945889492ee9b8c2a964f663134582 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 2 Jan 2024 19:12:32 +0800 Subject: [PATCH 011/297] add more comment --- src/pages/iou/request/step/IOURequestStepParticipants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 865695b9e38a..495853206dc0 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -59,6 +59,8 @@ function IOURequestStepParticipants({ if (!newIouType.current) { return; } + // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before + // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. IOU.updateMoneyRequestTypeParams(routes, newIouType.current); newIouType.current = null; }, [routes, participants]); From 2c34c91b7a413f5add1dec3e21822ec32715ee65 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 4 Jan 2024 16:31:54 +0800 Subject: [PATCH 012/297] update iou type for all money request page --- src/libs/actions/IOU.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index a641f0d061d4..59bf2ddbdc0e 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -174,12 +174,14 @@ function clearMoneyRequest(transactionID) { } /** + * Update money request-related pages IOU type params + * * @param {Object[]} routes * @param {String} newIouType */ function updateMoneyRequestTypeParams(routes, newIouType) { routes.forEach((route) => { - if (route.name !== SCREENS.MONEY_REQUEST.CREATE && route.name !== SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS) { + if (!route.name.startsWith('Money_Request_')) { return; } Navigation.setParams({iouType: newIouType}, route.key); From ccbc27eea6b5daa9bb6f11cb28affde1327e6d9e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 4 Jan 2024 16:39:56 +0800 Subject: [PATCH 013/297] remove unused import --- src/libs/actions/IOU.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 59bf2ddbdc0e..661c49379763 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -25,7 +25,6 @@ import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import * as Policy from './Policy'; import * as Report from './Report'; From cef767ab13aad2c2c04fee5db681915e70c59976 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 9 Jan 2024 13:49:16 +0100 Subject: [PATCH 014/297] ref: started migratiing MoneyRequestConfirmationList --- ...st.js => MoneyRequestConfirmationList.tsx} | 195 ++++++++---------- 1 file changed, 91 insertions(+), 104 deletions(-) rename src/components/{MoneyRequestConfirmationList.js => MoneyRequestConfirmationList.tsx} (88%) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.tsx similarity index 88% rename from src/components/MoneyRequestConfirmationList.js rename to src/components/MoneyRequestConfirmationList.tsx index 13dce9337673..5d0c12944d78 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -4,8 +4,8 @@ import {isEmpty} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -28,6 +28,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import * as OnyxTypes from '@src/types/onyx'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import categoryPropTypes from './categoryPropTypes'; import ConfirmedRoute from './ConfirmedRoute'; @@ -43,135 +44,115 @@ import tagPropTypes from './tagPropTypes'; import taxPropTypes from './taxPropTypes'; import Text from './Text'; import transactionPropTypes from './transactionPropTypes'; +import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; -const propTypes = { - /** Callback to inform parent modal of success */ - onConfirm: PropTypes.func, - - /** Callback to parent modal to send money */ - onSendMoney: PropTypes.func, - - /** Callback to inform a participant is selected */ - onSelectParticipant: PropTypes.func, - - /** Should we request a single or multiple participant selection from user */ - hasMultipleParticipants: PropTypes.bool.isRequired, - - /** IOU amount */ - iouAmount: PropTypes.number.isRequired, - - /** IOU comment */ - iouComment: PropTypes.string, - - /** IOU currency */ - iouCurrencyCode: PropTypes.string, - - /** IOU type */ - iouType: PropTypes.string, - - /** IOU date */ - iouCreated: PropTypes.string, - - /** IOU merchant */ - iouMerchant: PropTypes.string, - - /** IOU Category */ - iouCategory: PropTypes.string, +type MoneyRequestConfirmationListOnyxProps = { + iou?: OnyxTypes.IOU; + policyTaxRates?: any; + session: OnyxTypes.Session; + transaction?: OnyxTypes.Transaction; + mileageRate?: { + unit?: typeof CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES | typeof CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS; + rate?: number; + currency?: string; + }; + policyCategories?: any; + policyTags?: any; + policy?: OnyxEntry; +}; +type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & + WithCurrentUserPersonalDetailsProps & { + /** Callback to inform parent modal of success */ + onConfirm?: () => void; - /** IOU Tag */ - iouTag: PropTypes.string, + /** Callback to parent modal to send money */ + onSendMoney?: () => void; - /** IOU isBillable */ - iouIsBillable: PropTypes.bool, + /** Callback to inform a participant is selected */ + onSelectParticipant?: () => void; - /** Callback to toggle the billable state */ - onToggleBillable: PropTypes.func, + /** Should we request a single or multiple participant selection from user */ + hasMultipleParticipants: boolean; - /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired, + /** IOU amount */ + iouAmount: number; - /** Payee of the money request with login */ - payeePersonalDetails: optionPropTypes, + /** IOU comment */ + iouComment?: string; - /** Can the participants be modified or not */ - canModifyParticipants: PropTypes.bool, + /** IOU currency */ + iouCurrencyCode?: string; - /** Should the list be read only, and not editable? */ - isReadOnly: PropTypes.bool, + /** IOU type */ + iouType?: string; - /** Depending on expense report or personal IOU report, respective bank account route */ - bankAccountRoute: PropTypes.string, + /** IOU date */ + iouCreated?: string; - ...withCurrentUserPersonalDetailsPropTypes, + /** IOU merchant */ + iouMerchant?: string; - /** Current user session */ - session: PropTypes.shape({ - email: PropTypes.string.isRequired, - }), + /** IOU Category */ + iouCategory?: string; - /** The policyID of the request */ - policyID: PropTypes.string, + /** IOU Tag */ + iouTag?: string; - /** The reportID of the request */ - reportID: PropTypes.string, + /** IOU isBillable */ + iouIsBillable?: boolean; - /** File path of the receipt */ - receiptPath: PropTypes.string, + /** Callback to toggle the billable state */ + onToggleBillable?: () => void; - /** File name of the receipt */ - receiptFilename: PropTypes.string, + /** Selected participants from MoneyRequestModal with login / accountID */ + selectedParticipants: OptionType[]; - /** List styles for OptionsSelector */ - listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + /** Payee of the money request with login */ + payeePersonalDetails?: OptionType; - /** ID of the transaction that represents the money request */ - transactionID: PropTypes.string, + /** Can the participants be modified or not */ + canModifyParticipants?: boolean; - /** Transaction that represents the money request */ - transaction: transactionPropTypes, + /** Should the list be read only, and not editable? */ + isReadOnly?: boolean; - /** Unit and rate used for if the money request is a distance request */ - mileageRate: PropTypes.shape({ - /** Unit used to represent distance */ - unit: PropTypes.oneOf([CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]), + /** Depending on expense report or personal IOU report, respective bank account route */ + bankAccountRoute?: string; - /** Rate used to calculate the distance request amount */ - rate: PropTypes.number, + /** The policyID of the request */ + policyID?: string; - /** The currency of the rate */ - currency: PropTypes.string, - }), + /** The reportID of the request */ + reportID?: string; - /** Whether the money request is a distance request */ - isDistanceRequest: PropTypes.bool, + /** File path of the receipt */ + receiptPath?: string; - /** Whether the money request is a scan request */ - isScanRequest: PropTypes.bool, + /** File name of the receipt */ + receiptFilename?: string; - /** Whether we're editing a split bill */ - isEditingSplitBill: PropTypes.bool, + /** List styles for OptionsSelector */ + listStyles?: StyleProp; - /** Whether we should show the amount, date, and merchant fields. */ - shouldShowSmartScanFields: PropTypes.bool, + /** ID of the transaction that represents the money request */ + transactionID?: string; - /** A flag for verifying that the current report is a sub-report of a workspace chat */ - isPolicyExpenseChat: PropTypes.bool, + /** Whether the money request is a distance request */ + isDistanceRequest?: boolean; - /* Onyx Props */ - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), + /** Whether the money request is a scan request */ + isScanRequest?: boolean; - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, + /** Whether we're editing a split bill */ + isEditingSplitBill?: boolean; - /* Onyx Props */ - /** Collection of tax rates attached to a policy */ - policyTaxRates: taxPropTypes, + /** Whether we should show the amount, date, and merchant fields. */ + shouldShowSmartScanFields?: boolean; - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, -}; + /** A flag for verifying that the current report is a sub-report of a workspace chat */ + isPolicyExpenseChat?: boolean; + }; const defaultProps = { onConfirm: () => {}, @@ -208,12 +189,18 @@ const defaultProps = { policyTaxRates: {}, }; -function MoneyRequestConfirmationList(props) { +function MoneyRequestConfirmationList({ + onConfirm = () => {}, + onSendMoney = () => {}, + onSelectParticipant = () => {}, + iouType = CONST.IOU.TYPE.REQUEST, + iouCategory = '', + iouTag = '', + iouIsBillable = false, + onToggleBillable = () => {}, +}: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); - // Destructure functions from props to pass it as a dependecy to useCallback/useMemo hooks. - // Prop functions pass props itself as a "this" value to the function which means they change every time props change. - const {onSendMoney, onConfirm, onSelectParticipant} = props; const {translate, toLocaleDigit} = useLocalize(); const transaction = props.transaction; const {canUseViolations} = usePermissions(); From 0cfed1e596bfa3c2be19418b545645ff751f3443 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 10 Jan 2024 00:25:42 +0800 Subject: [PATCH 015/297] returns the onyx promise --- src/libs/actions/IOU.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f92b91dff539..bc3b9f490f34 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -270,9 +270,10 @@ function setMoneyRequestBillable_temporaryForRefactor(transactionID, billable) { /** * @param {String} transactionID * @param {Object[]} participants + * @returns {Promise} */ function setMoneyRequestParticipants_temporaryForRefactor(transactionID, participants) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); + return Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } /** From 7320bf70b5f3f0121f35cba641b9179cd6ccc6e9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 10 Jan 2024 00:26:12 +0800 Subject: [PATCH 016/297] use a promise approach to update the route params --- .../step/IOURequestStepParticipants.js | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 495853206dc0..894cc3ae0367 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -46,7 +46,6 @@ function IOURequestStepParticipants({ const headerTitle = isSplitRequest ? translate('iou.split') : translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); - const newIouType = useRef(null); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then @@ -55,29 +54,26 @@ function IOURequestStepParticipants({ IOUUtils.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID); }, [receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]); - useEffect(() => { - if (!newIouType.current) { - return; - } - // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before - // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. - IOU.updateMoneyRequestTypeParams(routes, newIouType.current); - newIouType.current = null; - }, [routes, participants]); - const addParticipant = useCallback( (val, isSplit) => { + let newIouType; // It's only possible to switch between REQUEST and SPLIT. // We want to update the IOU type only if it's not updated yet to prevent unnecessary updates. if (isSplit && iouType !== CONST.IOU.TYPE.SPLIT) { - newIouType.current = CONST.IOU.TYPE.SPLIT; + newIouType = CONST.IOU.TYPE.SPLIT; } else if (!isSplit && iouType === CONST.IOU.TYPE.SPLIT) { // Non-split can be either REQUEST or SEND. Instead of checking whether // the current IOU type is not a REQUEST (true for SEND), we check whether the current IOU type is a SPLIT. - newIouType.current = CONST.IOU.TYPE.REQUEST; + newIouType = CONST.IOU.TYPE.REQUEST; } - IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); + IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val).then(() => { + if (newIouType) { + // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before + // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. + IOU.updateMoneyRequestTypeParams(routes, newIouType); + } + }); numberOfParticipants.current = val.length; // When multiple participants are selected, the reportID is generated at the end of the confirmation step. @@ -90,7 +86,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = lodashGet(val, '[0].reportID', reportID); }, - [reportID, transactionID, iouType], + [reportID, transactionID, iouType, routes], ); const goToNextStep = useCallback( From 6875ca835e773017f9a4b195841433de1ef72033 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 10 Jan 2024 00:29:24 +0800 Subject: [PATCH 017/297] lint --- src/pages/iou/request/step/IOURequestStepParticipants.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 894cc3ae0367..aa8a3dac84e6 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -68,11 +68,12 @@ function IOURequestStepParticipants({ } IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val).then(() => { - if (newIouType) { - // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before - // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. - IOU.updateMoneyRequestTypeParams(routes, newIouType); + if (!newIouType) { + return; } + // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before + // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. + IOU.updateMoneyRequestTypeParams(routes, newIouType); }); numberOfParticipants.current = val.length; From d76e0dfcb8c726158f817641b432d93c4e53d330 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 9 Jan 2024 18:49:30 +0100 Subject: [PATCH 018/297] ref: keep migrating --- .../MoneyRequestConfirmationList.tsx | 522 +++++++++--------- .../TextInput/BaseTextInput/types.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/types/onyx/Policy.ts | 13 +- 4 files changed, 270 insertions(+), 269 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 5d0c12944d78..87c6db42c3b5 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,12 +1,12 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import {isEmpty} from 'lodash'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; @@ -22,55 +22,49 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {Participant} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import * as OnyxTypes from '@src/types/onyx'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {MileageRate} from '@src/types/onyx/Policy'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import categoryPropTypes from './categoryPropTypes'; import ConfirmedRoute from './ConfirmedRoute'; import FormHelpMessage from './FormHelpMessage'; import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; -import tagPropTypes from './tagPropTypes'; -import taxPropTypes from './taxPropTypes'; import Text from './Text'; -import transactionPropTypes from './transactionPropTypes'; import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; type MoneyRequestConfirmationListOnyxProps = { - iou?: OnyxTypes.IOU; - policyTaxRates?: any; + iou: OnyxEntry; + policyTaxRates: any; session: OnyxTypes.Session; - transaction?: OnyxTypes.Transaction; - mileageRate?: { - unit?: typeof CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES | typeof CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS; - rate?: number; - currency?: string; - }; - policyCategories?: any; - policyTags?: any; + transaction: OnyxTypes.Transaction; + mileageRate: OnyxEntry; + policyCategories: OnyxEntry; + policyTags: OnyxEntry; policy?: OnyxEntry; }; + type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & WithCurrentUserPersonalDetailsProps & { /** Callback to inform parent modal of success */ - onConfirm?: () => void; + onConfirm?: (selectedParticipants: Participant[]) => void; /** Callback to parent modal to send money */ - onSendMoney?: () => void; + onSendMoney?: (paymentMethod: ValueOf) => void; /** Callback to inform a participant is selected */ - onSelectParticipant?: () => void; + onSelectParticipant?: (option: Participant) => void; /** Should we request a single or multiple participant selection from user */ hasMultipleParticipants: boolean; @@ -85,7 +79,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & iouCurrencyCode?: string; /** IOU type */ - iouType?: string; + iouType?: ValueOf; /** IOU date */ iouCreated?: string; @@ -106,10 +100,10 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onToggleBillable?: () => void; /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: OptionType[]; + selectedParticipants: Participant[]; /** Payee of the money request with login */ - payeePersonalDetails?: OptionType; + payeePersonalDetails?: OnyxEntry; /** Can the participants be modified or not */ canModifyParticipants?: boolean; @@ -152,42 +146,11 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** A flag for verifying that the current report is a sub-report of a workspace chat */ isPolicyExpenseChat?: boolean; - }; -const defaultProps = { - onConfirm: () => {}, - onSendMoney: () => {}, - onSelectParticipant: () => {}, - iouType: CONST.IOU.TYPE.REQUEST, - iouCategory: '', - iouTag: '', - iouIsBillable: false, - onToggleBillable: () => {}, - payeePersonalDetails: null, - canModifyParticipants: false, - isReadOnly: false, - bankAccountRoute: '', - session: { - email: null, - }, - policyID: '', - reportID: '', - ...withCurrentUserPersonalDetailsDefaultProps, - receiptPath: '', - receiptFilename: '', - listStyles: [], - policyCategories: {}, - policyTags: {}, - transactionID: '', - transaction: {}, - mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, - isDistanceRequest: false, - isScanRequest: false, - shouldShowSmartScanFields: true, - isPolicyExpenseChat: false, - iou: iouDefaultProps, - policyTaxRates: {}, -}; + hasSmartScanFailed?: boolean; + + reportActionID?: string; + }; function MoneyRequestConfirmationList({ onConfirm = () => {}, @@ -198,81 +161,114 @@ function MoneyRequestConfirmationList({ iouTag = '', iouIsBillable = false, onToggleBillable = () => {}, + payeePersonalDetails = null, + canModifyParticipants = false, + isReadOnly = false, + bankAccountRoute = '', + policyID = '', + reportID = '', + receiptPath = '', + receiptFilename = '', + transactionID = '', + mileageRate = {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, + isDistanceRequest = false, + isScanRequest = false, + shouldShowSmartScanFields = true, + isPolicyExpenseChat = false, + transaction, + iouAmount, + policyTags, + policyCategories, + policy, + policyTaxRates, + iouCurrencyCode, + isEditingSplitBill, + hasSmartScanFailed, + iouMerchant, + currentUserPersonalDetails, + hasMultipleParticipants, + selectedParticipants, + session, + iou, + reportActionID, + iouCreated, + listStyles, + iouComment, }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const transaction = props.transaction; const {canUseViolations} = usePermissions(); - const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST; - const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT; - const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND; + const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; + const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const isTypeSend = iouType === CONST.IOU.TYPE.SEND; - const isSplitWithScan = isSplitBill && props.isScanRequest; + const isSplitWithScan = isSplitBill && isScanRequest; - const {unit, rate, currency} = props.mileageRate; - const distance = lodashGet(transaction, 'routes.route0.distance', 0); - const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; + const distance = transaction?.routes?.route0.distance ?? 0; + const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; // A flag for showing the categories field - const shouldShowCategories = props.isPolicyExpenseChat && (props.iouCategory || OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories))); + const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); // Do not hide fields in case of send money request - const shouldShowAllFields = props.isDistanceRequest || shouldExpandFields || !props.shouldShowSmartScanFields || isTypeSend || props.isEditingSplitBill; + const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; - const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !props.isDistanceRequest && !isSplitWithScan; + const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan; // Fetches the first tag list of the policy - const policyTag = PolicyUtils.getTag(props.policyTags); - const policyTagList = lodashGet(policyTag, 'tags', {}); - const policyTagListName = lodashGet(policyTag, 'name', translate('common.tag')); + const policyTag = PolicyUtils.getTag(policyTags); + const policyTagList = policyTag?.tags ?? {}; + const policyTagListName = policyTag?.name ?? translate('common.tag'); // A flag for showing the tags field - const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledOptions(_.values(policyTagList))); + const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledOptions(Object.values(policyTagList))); // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = props.isPolicyExpenseChat && props.policy.isTaxTrackingEnabled; + const shouldShowTax = isPolicyExpenseChat && policy?.isTaxTrackingEnabled; // A flag for showing the billable field - const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); + const shouldShowBillable = !policy?.disabledFields?.defaultBillable ?? true; const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithoutRoute = props.isDistanceRequest && !hasRoute; + const isDistanceRequestWithoutRoute = isDistanceRequest && !hasRoute; const formattedAmount = isDistanceRequestWithoutRoute ? translate('common.tbd') : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, - props.isDistanceRequest ? currency : props.iouCurrencyCode, + shouldCalculateDistanceAmount + ? DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate) + : iouAmount, + isDistanceRequest ? mileageRate?.currency : iouCurrencyCode, ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); - const defaultTaxKey = props.policyTaxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${props.policyTaxRates.taxes[defaultTaxKey].name} (${props.policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; - const taxRateTitle = (props.transaction.taxRate && props.transaction.taxRate.text) || defaultTaxName; + const defaultTaxKey = policyTaxRates.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; + const taxRateTitle = transaction.taxRate?.text || defaultTaxName; const isFocused = useIsFocused(); - const [formError, setFormError] = useState(''); + const [formError, setFormError] = useState(null); const [didConfirm, setDidConfirm] = useState(false); const [didConfirmSplit, setDidConfirmSplit] = useState(false); const shouldDisplayFieldError = useMemo(() => { - if (!props.isEditingSplitBill) { + if (!isEditingSplitBill) { return false; } - return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); - }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); + return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); + }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); - const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty; + const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + const shouldDisplayMerchantError = isPolicyExpenseChat && !isScanRequest && isMerchantEmpty; useEffect(() => { - if (shouldDisplayFieldError && props.hasSmartScanFailed) { + if (shouldDisplayFieldError && hasSmartScanFailed) { setFormError('iou.receiptScanningFailed'); return; } @@ -281,83 +277,80 @@ function MoneyRequestConfirmationList({ return; } // reset the form error whenever the screen gains or loses focus - setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, props.hasSmartScanFailed, didConfirmSplit]); + setFormError(null); + }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); useEffect(() => { if (!shouldCalculateDistanceAmount) { return; } - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate); + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate.rate); IOU.setMoneyRequestAmount(amount); - }, [shouldCalculateDistanceAmount, distance, rate, unit]); + }, [shouldCalculateDistanceAmount, distance, mileageRate?.rate, mileageRate?.unit]); /** * Returns the participants with amount - * @param {Array} participants - * @returns {Array} */ const getParticipantsWithAmount = useCallback( - (participantsList) => { - const iouAmount = IOUUtils.calculateAmount(participantsList.length, props.iouAmount, props.iouCurrencyCode); + (participantsList: Participant[]) => { + const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, - props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(iouAmount, props.iouCurrencyCode) : '', + calculatedIouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', ); }, - [props.iouAmount, props.iouCurrencyCode], + [iouAmount, iouCurrencyCode], ); // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again - if (props.isEditingSplitBill && didConfirm) { + if (isEditingSplitBill && didConfirm) { setDidConfirm(false); } const splitOrRequestOptions = useMemo(() => { let text; - if (isSplitBill && props.iouAmount === 0) { + if (isSplitBill && iouAmount === 0) { text = translate('iou.split'); - } else if ((props.receiptPath && isTypeRequest) || isDistanceRequestWithoutRoute) { + } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithoutRoute) { text = translate('iou.request'); - if (props.iouAmount !== 0) { - text = translate('iou.requestAmount', {amount: formattedAmount}); + if (iouAmount !== 0) { + text = translate('iou.requestAmount', {amount: Number(formattedAmount)}); } } else { const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.requestAmount'; - text = translate(translationKey, {amount: formattedAmount}); + text = translate(translationKey, {amount: Number(formattedAmount)}); } return [ { text: text[0].toUpperCase() + text.slice(1), - value: props.iouType, + value: iouType, }, ]; - }, [isSplitBill, isTypeRequest, props.iouType, props.iouAmount, props.receiptPath, formattedAmount, isDistanceRequestWithoutRoute, translate]); + }, [isSplitBill, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithoutRoute, translate]); - const selectedParticipants = useMemo(() => _.filter(props.selectedParticipants, (participant) => participant.selected), [props.selectedParticipants]); - const payeePersonalDetails = useMemo(() => props.payeePersonalDetails || props.currentUserPersonalDetails, [props.payeePersonalDetails, props.currentUserPersonalDetails]); - const canModifyParticipants = !props.isReadOnly && props.canModifyParticipants && props.hasMultipleParticipants; - const shouldDisablePaidBySection = canModifyParticipants; + const selectedParticipantsMemo = useMemo(() => selectedParticipants.filter((participant) => participant.selected), [selectedParticipants]); + const payeePersonalDetailsMemo = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); + const canModifyParticipantsValue = !isReadOnly && canModifyParticipants && hasMultipleParticipants; const optionSelectorSections = useMemo(() => { const sections = []; - const unselectedParticipants = _.filter(props.selectedParticipants, (participant) => !participant.selected); - if (props.hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants); + const unselectedParticipants = selectedParticipants.filter((participant) => !participant.selected); + if (hasMultipleParticipants) { + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipantsMemo); + let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; - if (!canModifyParticipants) { - formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({ + if (!canModifyParticipantsValue) { + formattedParticipantsList = formattedParticipantsList.map((participant) => ({ ...participant, isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), })); } - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, props.iouAmount, props.iouCurrencyCode, true); + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode, true); const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetails, - props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, props.iouCurrencyCode) : '', + payeePersonalDetailsMemo, + iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', ); sections.push( @@ -366,7 +359,7 @@ function MoneyRequestConfirmationList({ data: [formattedPayeeOption], shouldShow: true, indexOffset: 0, - isDisabled: shouldDisablePaidBySection, + isDisabled: canModifyParticipantsValue, }, { title: translate('moneyRequestConfirmationList.splitWith'), @@ -376,7 +369,7 @@ function MoneyRequestConfirmationList({ }, ); } else { - const formattedSelectedParticipants = _.map(props.selectedParticipants, (participant) => ({ + const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), })); @@ -389,50 +382,56 @@ function MoneyRequestConfirmationList({ } return sections; }, [ - props.selectedParticipants, - props.hasMultipleParticipants, - props.iouAmount, - props.iouCurrencyCode, - getParticipantsWithAmount, selectedParticipants, - payeePersonalDetails, + hasMultipleParticipants, + iouAmount, + iouCurrencyCode, + getParticipantsWithAmount, + payeePersonalDetailsMemo, translate, - shouldDisablePaidBySection, - canModifyParticipants, + canModifyParticipantsValue, + selectedParticipantsMemo, ]); const selectedOptions = useMemo(() => { - if (!props.hasMultipleParticipants) { + if (!hasMultipleParticipants) { return []; } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, props.hasMultipleParticipants, payeePersonalDetails]); + return [...selectedParticipantsMemo, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo)]; + }, [selectedParticipantsMemo, hasMultipleParticipants, payeePersonalDetailsMemo]); useEffect(() => { - if (!props.isDistanceRequest) { + if (!isDistanceRequest) { return; } - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); - IOU.setMoneyRequestMerchant_temporaryForRefactor(props.transactionID, distanceMerchant); - }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant( + hasRoute, + distance, + mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + mileageRate?.rate ?? 0, + mileageRate?.currency ?? 'USD', + translate, + toLocaleDigit, + ); + IOU.setMoneyRequestMerchant_temporaryForRefactor(transactionID, distanceMerchant); + }, [hasRoute, distance, mileageRate?.unit, mileageRate?.rate, mileageRate?.currency, translate, toLocaleDigit, isDistanceRequest, transactionID]); /** * @param {Object} option */ const selectParticipant = useCallback( - (option) => { + (option: Participant) => { // Return early if selected option is currently logged in user. - if (option.accountID === props.session.accountID) { + if (option.accountID === session?.accountID) { return; } onSelectParticipant(option); }, - [props.session.accountID, onSelectParticipant], + [session.accountID, onSelectParticipant], ); /** * Navigate to report details or profile of selected user - * @param {Object} option */ const navigateToReportOrUserDetail = (option) => { if (option.accountID) { @@ -444,19 +443,16 @@ function MoneyRequestConfirmationList({ } }; - /** - * @param {String} paymentMethod - */ const confirm = useCallback( - (paymentMethod) => { - if (_.isEmpty(selectedParticipants)) { + (paymentMethod: ValueOf) => { + if (selectedParticipantsMemo.length === 0) { return; } - if (props.iouCategory && props.iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { + if (iouCategory && iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { setFormError('iou.error.invalidCategoryLength'); return; } - if (props.iouType === CONST.IOU.TYPE.SEND) { + if (iouType === CONST.IOU.TYPE.SEND) { if (!paymentMethod) { return; } @@ -467,44 +463,44 @@ function MoneyRequestConfirmationList({ onSendMoney(paymentMethod); } else { // validate the amount for distance requests - const decimals = CurrencyUtils.getCurrencyDecimals(props.iouCurrencyCode); - if (props.isDistanceRequest && !isDistanceRequestWithoutRoute && !MoneyRequestUtils.validateAmount(String(props.iouAmount), decimals)) { + const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); + if (isDistanceRequest && !isDistanceRequestWithoutRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { setFormError('common.error.invalidAmount'); return; } - if (props.isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { + if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { setDidConfirmSplit(true); setFormError('iou.error.genericSmartscanFailureMessage'); return; } setDidConfirm(true); - onConfirm(selectedParticipants); + onConfirm(selectedParticipantsMemo); } }, [ - selectedParticipants, + selectedParticipantsMemo, + iouCategory, + iouType, onSendMoney, - onConfirm, - props.isEditingSplitBill, - props.iouType, - props.isDistanceRequest, - props.iouCategory, + iouCurrencyCode, + isDistanceRequest, isDistanceRequestWithoutRoute, - props.iouCurrencyCode, - props.iouAmount, + iouAmount, + isEditingSplitBill, transaction, + onConfirm, ], ); const footerContent = useMemo(() => { - if (props.isReadOnly) { + if (isReadOnly) { return; } - const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0 || shouldDisplayMerchantError; + const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; + const shouldDisableButton = selectedParticipantsMemo.length === 0 || shouldDisplayMerchantError; const button = shouldShowSettlementButton ? ( confirm(value)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} @@ -539,7 +536,7 @@ function MoneyRequestConfirmationList({ return ( <> - {!_.isEmpty(formError) && ( + {formError && ( ); }, [ - props.isReadOnly, - props.iouType, - props.bankAccountRoute, - props.iouCurrencyCode, - props.policyID, - selectedParticipants.length, + isReadOnly, + iouType, + selectedParticipantsMemo.length, shouldDisplayMerchantError, confirm, + bankAccountRoute, + iouCurrencyCode, + policyID, splitOrRequestOptions, formError, styles.ph1, @@ -565,81 +562,80 @@ function MoneyRequestConfirmationList({ translate, ]); - const {image: receiptImage, thumbnail: receiptThumbnail} = - props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {}; + const receiptData = ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename); return ( - {props.isDistanceRequest && ( + {isDistanceRequest && ( - + )} - {(receiptImage || receiptThumbnail) && ( + {(receiptData.image || receiptData.thumbnail) && ( )} - {props.shouldShowSmartScanFields && ( + {shouldShowSmartScanFields && ( { - if (props.isDistanceRequest) { + if (isDistanceRequest) { return; } - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.AMOUNT)); + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(props.iouType, props.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(iouType, reportID)); }} style={[styles.moneyRequestMenuItem, styles.mt2]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} /> )} { - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION)); + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.DESCRIPTION)); return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_DESCRIPTION.getRoute(props.iouType, props.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_DESCRIPTION.getRoute(iouType, reportID)); }} - style={[styles.moneyRequestMenuItem]} + style={styles.moneyRequestMenuItem} titleStyle={styles.flex1} disabled={didConfirm} - interactive={!props.isReadOnly} + interactive={!isReadOnly} numberOfLinesTitle={2} /> {!shouldShowAllFields && ( @@ -652,137 +648,133 @@ function MoneyRequestConfirmationList({ <> {shouldShowDate && ( { - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.DATE)); + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.DATE)); return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_DATE.getRoute(props.iouType, props.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_DATE.getRoute(iouType, reportID)); }} disabled={didConfirm} - interactive={!props.isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} /> )} - {props.isDistanceRequest && ( + {isDistanceRequest && ( Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(iouType, reportID))} disabled={didConfirm || !isTypeRequest} - interactive={!props.isReadOnly} + interactive={!isReadOnly} /> )} {shouldShowMerchant && ( { - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.MERCHANT)); + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.MERCHANT)); return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_MERCHANT.getRoute(props.iouType, props.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_MERCHANT.getRoute(iouType, reportID)); }} disabled={didConfirm} - interactive={!props.isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={shouldDisplayMerchantError || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction)) ? translate('common.error.enterMerchant') : ''} /> )} {shouldShowCategories && ( { - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.CATEGORY)); + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.CATEGORY)); return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_CATEGORY.getRoute(props.iouType, props.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_CATEGORY.getRoute(iouType, reportID)); }} - style={[styles.moneyRequestMenuItem]} + style={styles.moneyRequestMenuItem} titleStyle={styles.flex1} disabled={didConfirm} - interactive={!props.isReadOnly} - rightLabel={canUseViolations && Boolean(props.policy.requiresCategory) ? translate('common.required') : ''} + interactive={!isReadOnly} + rightLabel={canUseViolations && Boolean(policy?.requiresCategory) ? translate('common.required') : ''} /> )} {shouldShowTags && ( { - if (props.isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.TAG)); + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.TAG)); return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(iouType, reportID)); }} - style={[styles.moneyRequestMenuItem]} + style={styles.moneyRequestMenuItem} disabled={didConfirm} - interactive={!props.isReadOnly} - rightLabel={canUseViolations && Boolean(props.policy.requiresTag) ? translate('common.required') : ''} + interactive={!isReadOnly} + rightLabel={canUseViolations && Boolean(policy?.requiresTag) ? translate('common.required') : ''} /> )} {shouldShowTax && ( - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), - ) + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())) } disabled={didConfirm} - interactive={!props.isReadOnly} + interactive={!isReadOnly} /> )} {shouldShowTax && ( - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), - ) + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())) } disabled={didConfirm} - interactive={!props.isReadOnly} + interactive={!isReadOnly} /> )} {shouldShowBillable && ( - {translate('common.billable')} + {translate('common.billable')} )} @@ -792,13 +784,10 @@ function MoneyRequestConfirmationList({ ); } -MoneyRequestConfirmationList.propTypes = propTypes; -MoneyRequestConfirmationList.defaultProps = defaultProps; MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; export default compose( - withCurrentUserPersonalDetails, - withOnyx({ + withOnyx({ session: { key: ONYXKEYS.SESSION, }, @@ -825,4 +814,5 @@ export default compose( key: ONYXKEYS.IOU, }, }), + withCurrentUserPersonalDetails, )(MoneyRequestConfirmationList); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 21875d4dcc64..5564f3919542 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -111,7 +111,7 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; -type BaseTextInputRef = ForwardedRef>>; +type BaseTextInputRef = ForwardedRef>>; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0d7658adf180..a90a1f10a2f4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4534,4 +4534,4 @@ export { shouldDisableThread, }; -export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport}; +export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport, Participant}; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 2cd686c115b4..63091d983840 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -10,6 +10,12 @@ type Rate = { currency: string; }; +type MileageRate = { + unit: Unit; + rate: number; + currency: string; +}; + type CustomUnit = { customUnitID?: string; name?: string; @@ -79,8 +85,13 @@ type Policy = { /** The employee list of the policy */ employeeList?: []; + + /** Whether tax tracking enabled for policy */ + isTaxTrackingEnabled?: boolean; + + disabledFields?: Record; }; export default Policy; -export type {Unit}; +export type {Unit, MileageRate}; From eb8677e5005772586627d0787b9d237f21ca018a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 10 Jan 2024 12:21:16 +0700 Subject: [PATCH 019/297] create getUserDetailsTooltipText util --- src/components/MultipleAvatars.tsx | 18 +++++++++--------- src/libs/OptionsListUtils.js | 2 +- src/libs/ReportUtils.ts | 8 ++++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index a6f34cd459fc..82480ed0a4da 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -1,11 +1,10 @@ import React, {memo, useMemo} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {AvatarSource} from '@libs/UserUtils'; +import {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {Icon} from '@src/types/onyx/OnyxCommon'; @@ -13,6 +12,7 @@ import Avatar from './Avatar'; import Text from './Text'; import Tooltip from './Tooltip'; import UserDetailsTooltip from './UserDetailsTooltip'; +import * as ReportUtils from '@libs/ReportUtils'; type MultipleAvatarsProps = { /** Array of avatar URLs or icons */ @@ -143,7 +143,7 @@ function MultipleAvatars({ if (icons.length === 1 && !shouldStackHorizontally) { return ( ( maxAvatarsInRow && ( Number(icon.id)))} > {icons.length === 2 ? ( getDisplayNameForParticipant(accountIDs)).join(', '); +} + /** * For a deleted parent report action within a chat report, * let us return the appropriate display message @@ -4533,6 +4540,7 @@ export { shouldAutoFocusOnKeyPress, shouldDisplayThreadReplies, shouldDisableThread, + getUserDetailsTooltipText }; export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport, OptimisticCreatedReportAction}; From e0dc2e94a3912c965421e7304a8ec483373106ca Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 10 Jan 2024 08:07:25 +0100 Subject: [PATCH 020/297] ref: wip --- src/components/MoneyRequestConfirmationList.tsx | 5 +++-- src/types/onyx/PolicyTag.ts | 1 + src/types/onyx/Transaction.ts | 12 ++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 87c6db42c3b5..741118167a26 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -240,7 +240,7 @@ function MoneyRequestConfirmationList({ ? translate('common.tbd') : CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount - ? DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate) + ? DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate ?? 0) : iouAmount, isDistanceRequest ? mileageRate?.currency : iouCurrencyCode, ); @@ -248,6 +248,7 @@ function MoneyRequestConfirmationList({ const defaultTaxKey = policyTaxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const taxRateTitle = transaction.taxRate?.text || defaultTaxName; const isFocused = useIsFocused(); @@ -347,7 +348,7 @@ function MoneyRequestConfirmationList({ })); } - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode, true); + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( payeePersonalDetailsMemo, iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 58a21dcf4df5..e9b909b1748a 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -8,6 +8,7 @@ type PolicyTag = { /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ // eslint-disable-next-line @typescript-eslint/naming-convention 'GL Code': string; + tags: PolicyTags; }; type PolicyTags = Record; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 8b7e26280305..179f473dc71f 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -3,6 +3,15 @@ import type CONST from '@src/CONST'; import type * as OnyxCommon from './OnyxCommon'; import type RecentWaypoint from './RecentWaypoint'; +type TaxRate = { + text: string; + keyForList: string; + data: { + value: string; + }; + modifiedName?: string; +}; + type Waypoint = { /** The name associated with the address of the waypoint */ name?: string; @@ -18,6 +27,7 @@ type Waypoint = { }; type WaypointCollection = Record; + type Comment = { comment?: string; waypoints?: WaypointCollection; @@ -94,6 +104,8 @@ type Transaction = { /** If the transaction was made in a foreign currency, we send the original amount and currency */ originalAmount?: number; originalCurrency?: string; + taxAmount?: number; + taxRate?: TaxRate; }; export default Transaction; From ebabfc77dd2ed34b17d8add8ef72d9c7e66f5cd1 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 10 Jan 2024 14:42:32 +0700 Subject: [PATCH 021/297] fix lint --- src/components/MultipleAvatars.tsx | 13 ++++++++----- .../BaseUserDetailsTooltip/index.tsx | 4 ++-- src/libs/ReportUtils.ts | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 82480ed0a4da..5f9575e09bfd 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -1,10 +1,12 @@ import React, {memo, useMemo} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; -import {ValueOf} from 'type-fest'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {AvatarSource} from '@libs/UserUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {Icon} from '@src/types/onyx/OnyxCommon'; @@ -12,7 +14,6 @@ import Avatar from './Avatar'; import Text from './Text'; import Tooltip from './Tooltip'; import UserDetailsTooltip from './UserDetailsTooltip'; -import * as ReportUtils from '@libs/ReportUtils'; type MultipleAvatarsProps = { /** Array of avatar URLs or icons */ @@ -214,7 +215,9 @@ function MultipleAvatars({ {avatars.length > maxAvatarsInRow && ( Number(icon.id)))} + text={ReportUtils.getUserDetailsTooltipText( + icons.slice(avatarRows.length * maxAvatarsInRow - 1, avatarRows.length * maxAvatarsInRow + 9).map((icon) => Number(icon.id)), + )} > getDisplayNameForParticipant(accountIDs)).join(', '); + return accountIDs.map((accountID) => getDisplayNameForParticipant(accountID)).join(', '); } /** @@ -4540,7 +4540,7 @@ export { shouldAutoFocusOnKeyPress, shouldDisplayThreadReplies, shouldDisableThread, - getUserDetailsTooltipText + getUserDetailsTooltipText, }; export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport, OptimisticCreatedReportAction}; From 83b98ea8d1d4749ea7246faff24686ed7e874839 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 10 Jan 2024 15:24:06 +0700 Subject: [PATCH 022/297] fix ts lint --- src/components/MultipleAvatars.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 5f9575e09bfd..48a9bd77c09e 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -144,7 +144,7 @@ function MultipleAvatars({ if (icons.length === 1 && !shouldStackHorizontally) { return ( ( {icons.length === 2 ? ( Date: Wed, 10 Jan 2024 16:29:28 +0100 Subject: [PATCH 023/297] fix: wip --- src/ONYXKEYS.ts | 1 + src/components/MenuItem.tsx | 2 +- .../MoneyRequestConfirmationList.tsx | 108 +++++++++--------- src/libs/ReceiptUtils.ts | 5 +- src/libs/TransactionUtils.ts | 19 +-- src/types/onyx/Policy.ts | 4 + src/types/onyx/PolicyTaxRates.ts | 4 + src/types/onyx/index.ts | 4 + 8 files changed, 83 insertions(+), 64 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 89ddbdc06883..7fb29f348a44 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -439,6 +439,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; + [ONYXKEYS.COLLECTION.POLICY_TAX_RATE]: OnyxTypes.PolicyTaxRate; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index ce44db72598a..9b35a0963b14 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -204,7 +204,7 @@ type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { shouldBlockSelection?: boolean; /** Whether should render title as HTML or as Text */ - shouldParseTitle?: false; + shouldParseTitle?: boolean; /** Should check anonymous user in onPress function */ shouldCheckActionAllowedOnPress?: boolean; diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 741118167a26..b0a9ce1d7a91 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -11,7 +11,6 @@ import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; @@ -46,13 +45,12 @@ import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; type MoneyRequestConfirmationListOnyxProps = { iou: OnyxEntry; - policyTaxRates: any; - session: OnyxTypes.Session; - transaction: OnyxTypes.Transaction; + policyTaxRates: OnyxEntry; + session: OnyxEntry; mileageRate: OnyxEntry; - policyCategories: OnyxEntry; + policyCategories: OnyxEntry; policyTags: OnyxEntry; - policy?: OnyxEntry; + policy: OnyxEntry; }; type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & @@ -150,6 +148,8 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & hasSmartScanFailed?: boolean; reportActionID?: string; + + transaction: OnyxTypes.Transaction; }; function MoneyRequestConfirmationList({ @@ -210,7 +210,7 @@ function MoneyRequestConfirmationList({ const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories))); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -244,12 +244,12 @@ function MoneyRequestConfirmationList({ : iouAmount, isDistanceRequest ? mileageRate?.currency : iouCurrencyCode, ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - const defaultTaxKey = policyTaxRates.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; + const defaultTaxKey = policyTaxRates?.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates?.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) ?? ''; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const taxRateTitle = transaction.taxRate?.text || defaultTaxName; + const taxRateTitle = transaction?.taxRate?.text || defaultTaxName; const isFocused = useIsFocused(); const [formError, setFormError] = useState(null); @@ -286,7 +286,7 @@ function MoneyRequestConfirmationList({ return; } - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate.rate); + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate); IOU.setMoneyRequestAmount(amount); }, [shouldCalculateDistanceAmount, distance, mileageRate?.rate, mileageRate?.unit]); @@ -398,8 +398,10 @@ function MoneyRequestConfirmationList({ if (!hasMultipleParticipants) { return []; } - return [...selectedParticipantsMemo, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo)]; - }, [selectedParticipantsMemo, hasMultipleParticipants, payeePersonalDetailsMemo]); + // TODO: check if this is needed + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); + return [...selectedParticipantsMemo, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo, String(myIOUAmount))]; + }, [hasMultipleParticipants, selectedParticipantsMemo, iouAmount, iouCurrencyCode, payeePersonalDetailsMemo]); useEffect(() => { if (!isDistanceRequest) { @@ -428,14 +430,14 @@ function MoneyRequestConfirmationList({ } onSelectParticipant(option); }, - [session.accountID, onSelectParticipant], + [session?.accountID, onSelectParticipant], ); /** * Navigate to report details or profile of selected user */ - const navigateToReportOrUserDetail = (option) => { - if (option.accountID) { + const navigateToReportOrUserDetail = (option: Participant | OnyxTypes.Report) => { + if ('accountID' in option) { const activeRoute = Navigation.getActiveRouteWithoutParams(); Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); @@ -505,6 +507,7 @@ function MoneyRequestConfirmationList({ const button = shouldShowSettlementButton ? ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())) + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), + ) } disabled={didConfirm} interactive={!isReadOnly} @@ -758,11 +764,13 @@ function MoneyRequestConfirmationList({ - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())) + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), + ) } disabled={didConfirm} interactive={!isReadOnly} @@ -787,33 +795,29 @@ function MoneyRequestConfirmationList({ MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; -export default compose( - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - }, - policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - }, - mileageRate: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - selector: DistanceRequestUtils.getDefaultMileageRate, - }, - splitTransactionDraft: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - policyTaxRates: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${policyID}`, - }, - iou: { - key: ONYXKEYS.IOU, - }, - }), - withCurrentUserPersonalDetails, -)(MoneyRequestConfirmationList); +const MoneyRequestConfirmationListWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(MoneyRequestConfirmationList); + +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + policyCategories: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + }, + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, + mileageRate: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: DistanceRequestUtils.getDefaultMileageRate, + }, + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + policyTaxRates: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${policyID}`, + }, + iou: { + key: ONYXKEYS.IOU, + }, +})(MoneyRequestConfirmationListWithCurrentUserPersonalDetails); diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index bcba68a3a0bd..7858c3d7277d 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,5 +1,6 @@ import Str from 'expensify-common/lib/str'; import type {ImageSourcePropType} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import ReceiptDoc from '@assets/images/receipt-doc.png'; import ReceiptGeneric from '@assets/images/receipt-generic.png'; import ReceiptHTML from '@assets/images/receipt-html.png'; @@ -28,7 +29,7 @@ type FileNameAndExtension = { * @param receiptPath * @param receiptFileName */ -function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { +function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI @@ -39,7 +40,7 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string if (!Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction?.transactionID), transaction}; } // For local files, we won't have a thumbnail yet diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index c34a6753c1d5..48a7d4065700 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -139,23 +139,24 @@ function hasReceipt(transaction: Transaction | undefined | null): boolean { return !!transaction?.receipt?.state || hasEReceipt(transaction); } -function isMerchantMissing(transaction: Transaction) { - const isMerchantEmpty = transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.merchant === ''; +function isMerchantMissing(transaction: OnyxEntry) { + const isMerchantEmpty = transaction?.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction?.merchant === ''; - const isModifiedMerchantEmpty = !transaction.modifiedMerchant || transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.modifiedMerchant === ''; + const isModifiedMerchantEmpty = + !transaction?.modifiedMerchant || transaction?.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction?.modifiedMerchant === ''; return isMerchantEmpty && isModifiedMerchantEmpty; } -function isAmountMissing(transaction: Transaction) { - return transaction.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0); +function isAmountMissing(transaction: OnyxEntry) { + return transaction?.amount === 0 && (!transaction?.modifiedAmount || transaction?.modifiedAmount === 0); } -function isCreatedMissing(transaction: Transaction) { - return transaction.created === '' && (!transaction.created || transaction.modifiedCreated === ''); +function isCreatedMissing(transaction: OnyxEntry) { + return transaction?.created === '' && (!transaction?.created || transaction?.modifiedCreated === ''); } -function areRequiredFieldsEmpty(transaction: Transaction): boolean { +function areRequiredFieldsEmpty(transaction: OnyxEntry): boolean { const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`] ?? null; const isFromExpenseReport = parentReport?.type === CONST.REPORT.TYPE.EXPENSE; return (isFromExpenseReport && isMerchantMissing(transaction)) || isAmountMissing(transaction) || isCreatedMissing(transaction); @@ -434,7 +435,7 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean /** * Check if the transaction has a defined route */ -function hasRoute(transaction: Transaction): boolean { +function hasRoute(transaction: OnyxEntry): boolean { return !!transaction?.routes?.route0?.geometry?.coordinates; } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 63091d983840..af65dd57607f 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -90,6 +90,10 @@ type Policy = { isTaxTrackingEnabled?: boolean; disabledFields?: Record; + + requiresCategory?: boolean; + + requiresTag?: boolean; }; export default Policy; diff --git a/src/types/onyx/PolicyTaxRates.ts b/src/types/onyx/PolicyTaxRates.ts index d549b620f51f..492cf37a3181 100644 --- a/src/types/onyx/PolicyTaxRates.ts +++ b/src/types/onyx/PolicyTaxRates.ts @@ -7,6 +7,10 @@ type PolicyTaxRate = { /** Whether the tax is disabled */ isDisabled?: boolean; + + defaultExternalID: string; + + taxes: PolicyTaxRates; }; type PolicyTaxRates = Record; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..2ef702226384 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -32,6 +32,8 @@ import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; import type {PolicyTag, PolicyTags} from './PolicyTag'; +import type {PolicyTaxRates} from './PolicyTaxRates'; +import type PolicyTaxRate from './PolicyTaxRates'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -137,4 +139,6 @@ export type { ReportUserIsTyping, PolicyReportField, RecentlyUsedReportFields, + PolicyTaxRate, + PolicyTaxRates, }; From 4ab2ec1000b86078ea558e2d8fae6b71b6c4d444 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 11 Jan 2024 17:12:11 +0700 Subject: [PATCH 024/297] Change logic page not found in profile page --- src/pages/ProfilePage.js | 239 ++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 126 deletions(-) diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index c0c782f176ca..590a4635f9e5 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -8,12 +8,11 @@ import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import AutoUpdateTime from '@components/AutoUpdateTime'; import Avatar from '@components/Avatar'; -import BlockingView from '@components/BlockingViews/BlockingView'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import CommunicationsLink from '@components/CommunicationsLink'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; -import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -30,7 +29,6 @@ import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; -import variables from '@styles/variables'; import * as PersonalDetails from '@userActions/PersonalDetails'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; @@ -124,9 +122,6 @@ function ProfilePage(props) { const hasMinimumDetails = !_.isEmpty(details.avatar); const isLoading = lodashGet(details, 'isLoading', false) || _.isEmpty(details); - // If the API returns an error for some reason there won't be any details and isLoading will get set to false, so we want to show a blocking screen - const shouldShowBlockingView = !hasMinimumDetails && !isLoading; - const statusEmojiCode = lodashGet(details, 'status.emojiCode', ''); const statusText = lodashGet(details, 'status.text', ''); const hasStatus = !!statusEmojiCode; @@ -146,132 +141,124 @@ function ProfilePage(props) { return ( - Navigation.goBack(navigateBackTo)} - /> - - {hasMinimumDetails && ( - - - - {({show}) => ( - - - - - - )} - - {Boolean(displayName) && ( - + Navigation.goBack(navigateBackTo)} + /> + + {hasMinimumDetails && ( + + + - {displayName} - - )} - {hasStatus && ( - + {({show}) => ( + + + + + + )} + + {Boolean(displayName) && ( - {props.translate('statusPage.status')} + {displayName} - {statusContent} - - )} + )} + {hasStatus && ( + + + {props.translate('statusPage.status')} + + {statusContent} + + )} - {login ? ( - - - {props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')} - - - - {isSMSLogin ? props.formatPhoneNumber(phoneNumber) : login} - - - - ) : null} - {pronouns ? ( - - - {props.translate('profilePage.preferredPronouns')} - - {pronouns} - - ) : null} - {shouldShowLocalTime && } - - {shouldShowNotificationPreference && ( - Navigation.navigate(ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.getRoute(props.report.reportID))} - wrapperStyle={[styles.mtn6, styles.mb5]} - /> - )} - {!isCurrentUser && !Session.isAnonymousUser() && ( - Report.navigateToAndOpenReportWithAccountIDs([accountID])} - wrapperStyle={styles.breakAll} - shouldShowRightIcon - /> - )} - {!_.isEmpty(props.report) && ( - ReportUtils.navigateToPrivateNotes(props.report, props.session)} - wrapperStyle={styles.breakAll} - shouldShowRightIcon - brickRoadIndicator={Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} - /> - )} - - )} - {!hasMinimumDetails && isLoading && } - {shouldShowBlockingView && ( - - )} - + {login ? ( + + + {props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')} + + + + {isSMSLogin ? props.formatPhoneNumber(phoneNumber) : login} + + + + ) : null} + {pronouns ? ( + + + {props.translate('profilePage.preferredPronouns')} + + {pronouns} + + ) : null} + {shouldShowLocalTime && } + + {shouldShowNotificationPreference && ( + Navigation.navigate(ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.getRoute(props.report.reportID))} + wrapperStyle={[styles.mtn6, styles.mb5]} + /> + )} + {!isCurrentUser && !Session.isAnonymousUser() && ( + Report.navigateToAndOpenReportWithAccountIDs([accountID])} + wrapperStyle={styles.breakAll} + shouldShowRightIcon + /> + )} + {!_.isEmpty(props.report) && ( + ReportUtils.navigateToPrivateNotes(props.report, props.session)} + wrapperStyle={styles.breakAll} + shouldShowRightIcon + brickRoadIndicator={Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + /> + )} + + )} + {!hasMinimumDetails && isLoading && } + + ); } From ef6ae5f2735ca7cce0441454828f70b3be5210a3 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 11 Jan 2024 15:30:18 +0100 Subject: [PATCH 025/297] Update policyCategories type in MoneyRequestConfirmationList --- src/ONYXKEYS.ts | 2 +- .../MoneyRequestConfirmationList.tsx | 60 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 45f70d1864bc..f5e8bba63361 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -437,7 +437,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; - [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; + [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; [ONYXKEYS.COLLECTION.POLICY_TAX_RATE]: OnyxTypes.PolicyTaxRate; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b0a9ce1d7a91..9c46144d3a56 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -46,9 +46,8 @@ import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; type MoneyRequestConfirmationListOnyxProps = { iou: OnyxEntry; policyTaxRates: OnyxEntry; - session: OnyxEntry; mileageRate: OnyxEntry; - policyCategories: OnyxEntry; + policyCategories: OnyxEntry; policyTags: OnyxEntry; policy: OnyxEntry; }; @@ -210,7 +209,8 @@ function MoneyRequestConfirmationList({ const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories))); + const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -286,7 +286,7 @@ function MoneyRequestConfirmationList({ return; } - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate); + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate ?? 0); IOU.setMoneyRequestAmount(amount); }, [shouldCalculateDistanceAmount, distance, mileageRate?.rate, mileageRate?.unit]); @@ -299,7 +299,8 @@ function MoneyRequestConfirmationList({ return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, calculatedIouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', - ); + // TODO: Remove assertion after OptionsListUtils will be migrated + ) as Participant[]; }, [iouAmount, iouCurrencyCode], ); @@ -795,29 +796,26 @@ function MoneyRequestConfirmationList({ MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; -const MoneyRequestConfirmationListWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(MoneyRequestConfirmationList); - -export default withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - }, - policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - }, - mileageRate: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - selector: DistanceRequestUtils.getDefaultMileageRate, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - policyTaxRates: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${policyID}`, - }, - iou: { - key: ONYXKEYS.IOU, - }, -})(MoneyRequestConfirmationListWithCurrentUserPersonalDetails); +export default withCurrentUserPersonalDetails( + withOnyx({ + policyCategories: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + }, + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, + mileageRate: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: DistanceRequestUtils.getDefaultMileageRate, + }, + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + policyTaxRates: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAX_RATE}${policyID}`, + }, + iou: { + key: ONYXKEYS.IOU, + }, + })(MoneyRequestConfirmationList), +); From 441e437de6ab31f4b80a4ccf5f4b8ddddfcf47c7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 15 Jan 2024 08:40:17 +0100 Subject: [PATCH 026/297] fix: wip --- src/components/MoneyRequestConfirmationList.tsx | 2 +- src/components/TextInput/BaseTextInput/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 9c46144d3a56..5e292e310313 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -646,7 +646,7 @@ function MoneyRequestConfirmationList({ /> {!shouldShowAllFields && ( )} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 5564f3919542..21875d4dcc64 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -111,7 +111,7 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; -type BaseTextInputRef = ForwardedRef>>; +type BaseTextInputRef = ForwardedRef>>; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; From 814715b85a10ba75bc517946404639a7b96d4939 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 15 Jan 2024 09:02:53 +0100 Subject: [PATCH 027/297] fix: wip --- src/components/MoneyRequestConfirmationList.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 5e292e310313..c66a58625300 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -44,11 +44,22 @@ import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersona import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; type MoneyRequestConfirmationListOnyxProps = { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: OnyxEntry; + + /** Collection of tax rates attached to a policy */ policyTaxRates: OnyxEntry; + + /** Unit and rate used for if the money request is a distance request */ mileageRate: OnyxEntry; + + /** Collection of categories attached to a policy */ policyCategories: OnyxEntry; + + /** Collection of tags attached to a policy */ policyTags: OnyxEntry; + + /** The policy of root parent report */ policy: OnyxEntry; }; @@ -169,7 +180,7 @@ function MoneyRequestConfirmationList({ receiptPath = '', receiptFilename = '', transactionID = '', - mileageRate = {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, + mileageRate, isDistanceRequest = false, isScanRequest = false, shouldShowSmartScanFields = true, @@ -399,7 +410,6 @@ function MoneyRequestConfirmationList({ if (!hasMultipleParticipants) { return []; } - // TODO: check if this is needed const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); return [...selectedParticipantsMemo, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo, String(myIOUAmount))]; }, [hasMultipleParticipants, selectedParticipantsMemo, iouAmount, iouCurrencyCode, payeePersonalDetailsMemo]); @@ -420,9 +430,6 @@ function MoneyRequestConfirmationList({ IOU.setMoneyRequestMerchant_temporaryForRefactor(transactionID, distanceMerchant); }, [hasRoute, distance, mileageRate?.unit, mileageRate?.rate, mileageRate?.currency, translate, toLocaleDigit, isDistanceRequest, transactionID]); - /** - * @param {Object} option - */ const selectParticipant = useCallback( (option: Participant) => { // Return early if selected option is currently logged in user. From e5921cee015660fa3a5b59b453a9617dbeefa53a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 17 Jan 2024 10:39:47 +0100 Subject: [PATCH 028/297] fix: resolve comments --- src/components/ConfirmedRoute.tsx | 6 +- .../MoneyRequestConfirmationList.tsx | 65 ++++++++++++------- src/libs/ReportUtils.ts | 5 +- src/types/onyx/OriginalMessage.ts | 4 +- src/types/onyx/Policy.ts | 3 + src/types/onyx/PolicyTaxRates.ts | 2 + 6 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index c01f7c6250f4..8a3b39305dba 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -30,13 +30,13 @@ type ConfirmedRoutePropsOnyxProps = { type ConfirmedRouteProps = ConfirmedRoutePropsOnyxProps & { /** Transaction that stores the distance request data */ - transaction: Transaction; + transaction: Transaction | undefined; }; function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) { const {isOffline} = useNetwork(); - const {route0: route} = transaction.routes ?? {}; - const waypoints = transaction.comment?.waypoints ?? {}; + const {route0: route} = transaction?.routes ?? {}; + const waypoints = transaction?.comment?.waypoints ?? {}; const coordinates = route?.geometry?.coordinates ?? []; const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index c66a58625300..f1d042b29cad 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,6 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; -import {isEmpty} from 'lodash'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; @@ -29,6 +28,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +import type {PaymentType} from '@src/types/onyx/OriginalMessage'; import type {MileageRate} from '@src/types/onyx/Policy'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import ConfirmedRoute from './ConfirmedRoute'; @@ -43,6 +43,15 @@ import Text from './Text'; import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; +type Option = Partial; + +type CategorySection = { + title: string | undefined; + shouldShow: boolean; + indexOffset: number; + data: Option[]; +}; + type MoneyRequestConfirmationListOnyxProps = { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: OnyxEntry; @@ -69,7 +78,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onConfirm?: (selectedParticipants: Participant[]) => void; /** Callback to parent modal to send money */ - onSendMoney?: (paymentMethod: ValueOf) => void; + onSendMoney?: (paymentMethod: PaymentType) => void; /** Callback to inform a participant is selected */ onSelectParticipant?: (option: Participant) => void; @@ -155,11 +164,14 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** A flag for verifying that the current report is a sub-report of a workspace chat */ isPolicyExpenseChat?: boolean; + /** Whether there is smartscan failed */ hasSmartScanFailed?: boolean; + /** ID of the report action */ reportActionID?: string; - transaction: OnyxTypes.Transaction; + /** Transaction object */ + transaction?: OnyxTypes.Transaction; }; function MoneyRequestConfirmationList({ @@ -171,7 +183,7 @@ function MoneyRequestConfirmationList({ iouTag = '', iouIsBillable = false, onToggleBillable = () => {}, - payeePersonalDetails = null, + payeePersonalDetails, canModifyParticipants = false, isReadOnly = false, bankAccountRoute = '', @@ -245,7 +257,7 @@ function MoneyRequestConfirmationList({ // A flag for showing the billable field const shouldShowBillable = !policy?.disabledFields?.defaultBillable ?? true; - const hasRoute = TransactionUtils.hasRoute(transaction); + const hasRoute = TransactionUtils.hasRoute(transaction ?? null); const isDistanceRequestWithoutRoute = isDistanceRequest && !hasRoute; const formattedAmount = isDistanceRequestWithoutRoute ? translate('common.tbd') @@ -273,7 +285,7 @@ function MoneyRequestConfirmationList({ return false; } - return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); + return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction ?? null)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)); }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; @@ -310,7 +322,7 @@ function MoneyRequestConfirmationList({ return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, calculatedIouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', - // TODO: Remove assertion after OptionsListUtils will be migrated + // TODO: Remove the assertion once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. ) as Participant[]; }, [iouAmount, iouCurrencyCode], @@ -346,7 +358,7 @@ function MoneyRequestConfirmationList({ const payeePersonalDetailsMemo = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); const canModifyParticipantsValue = !isReadOnly && canModifyParticipants && hasMultipleParticipants; - const optionSelectorSections = useMemo(() => { + const optionSelectorSections: CategorySection[] = useMemo(() => { const sections = []; const unselectedParticipants = selectedParticipants.filter((participant) => !participant.selected); if (hasMultipleParticipants) { @@ -445,17 +457,17 @@ function MoneyRequestConfirmationList({ * Navigate to report details or profile of selected user */ const navigateToReportOrUserDetail = (option: Participant | OnyxTypes.Report) => { - if ('accountID' in option) { + if ('accountID' in option && option.accountID) { const activeRoute = Navigation.getActiveRouteWithoutParams(); Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); - } else if (option.reportID) { + } else if ('reportID' in option && option.reportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); } }; const confirm = useCallback( - (paymentMethod: ValueOf) => { + (paymentMethod: PaymentType) => { if (selectedParticipantsMemo.length === 0) { return; } @@ -480,7 +492,7 @@ function MoneyRequestConfirmationList({ return; } - if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { + if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { setDidConfirmSplit(true); setFormError('iou.error.genericSmartscanFailureMessage'); return; @@ -540,7 +552,7 @@ function MoneyRequestConfirmationList({ pressOnEnter isDisabled={shouldDisableButton} // eslint-disable-next-line @typescript-eslint/naming-convention - onPress={(_event, value) => confirm(value)} + onPress={(_, value) => confirm(value)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} /> @@ -574,7 +586,7 @@ function MoneyRequestConfirmationList({ translate, ]); - const receiptData = ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename); + const receiptData = ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename); return ( // @ts-expect-error TODO: Remove this once OptionsSelector (https://github.com/Expensify/App/issues/25125) is migrated to TypeScript. @@ -626,11 +638,11 @@ function MoneyRequestConfirmationList({ } Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(iouType, reportID)); }} - style={{...styles.moneyRequestMenuItem, ...styles.mt2}} + style={[styles.moneyRequestMenuItem, styles.mt2]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} /> )} )} {isDistanceRequest && ( @@ -707,8 +720,12 @@ function MoneyRequestConfirmationList({ }} disabled={didConfirm} interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayMerchantError || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction)) ? translate('common.error.enterMerchant') : ''} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={ + shouldDisplayMerchantError || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null)) + ? translate('common.error.enterMerchant') + : '' + } /> )} {shouldShowCategories && ( @@ -747,7 +764,7 @@ function MoneyRequestConfirmationList({ style={styles.moneyRequestMenuItem} disabled={didConfirm} interactive={!isReadOnly} - rightLabel={canUseViolations && Boolean(policy?.requiresTag) ? translate('common.required') : ''} + rightLabel={canUseViolations && !!policy?.requiresTag ? translate('common.required') : ''} /> )} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5b3347dbfa3e..ecccb451b17f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,12 +16,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, PaymentType} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference} from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -2698,7 +2697,7 @@ function buildOptimisticIOUReportAction( comment: string, participants: Participant[], transactionID: string, - paymentType: DeepValueOf, + paymentType: PaymentType, iouReportID = '', isSettlingUp = false, isSendMoneyFlow = false, diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f10696ced00f..5549b96acdb2 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -2,6 +2,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; +type PaymentType = DeepValueOf; type ActionName = DeepValueOf; type OriginalMessageActionName = | 'ADDCOMMENT' @@ -43,7 +44,7 @@ type IOUMessage = { lastModified?: string; participantAccountIDs?: number[]; type: ValueOf; - paymentType?: DeepValueOf; + paymentType?: PaymentType; /** Only exists when we are sending money */ IOUDetails?: IOUDetails; }; @@ -280,4 +281,5 @@ export type { OriginalMessageIOU, OriginalMessageCreated, OriginalMessageAddComment, + PaymentType, }; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index a2182298acdf..19cecf3b9e6d 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -92,10 +92,13 @@ type Policy = { /** Whether tax tracking enabled for policy */ isTaxTrackingEnabled?: boolean; + /** List of fields which should disabled for the policy */ disabledFields?: Record; + /** Whether or not the policy requires categories */ requiresCategory?: boolean; + /** Whether or not the policy requires tags */ requiresTag?: boolean; }; diff --git a/src/types/onyx/PolicyTaxRates.ts b/src/types/onyx/PolicyTaxRates.ts index 492cf37a3181..5709ce6338ab 100644 --- a/src/types/onyx/PolicyTaxRates.ts +++ b/src/types/onyx/PolicyTaxRates.ts @@ -8,8 +8,10 @@ type PolicyTaxRate = { /** Whether the tax is disabled */ isDisabled?: boolean; + /** Default policy tax ID */ defaultExternalID: string; + /** List of tax names and values */ taxes: PolicyTaxRates; }; From 1e8778c4ec1f2fc0b083f2bc0c33fa237ccb88ae Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 16:44:02 +0700 Subject: [PATCH 029/297] fix: status bar style not update --- .../CustomStatusBarAndBackground/index.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index f66a0204ac5e..9ca5edeb7ed8 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -3,6 +3,7 @@ import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDela import useTheme from '@hooks/useTheme'; import {navigationRef} from '@libs/Navigation/Navigation'; import StatusBar from '@libs/StatusBar'; +import {StatusBarStyle} from '@styles/index'; import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext'; import updateGlobalBackgroundColor from './updateGlobalBackgroundColor'; import updateStatusBarAppearance from './updateStatusBarAppearance'; @@ -16,7 +17,7 @@ type CustomStatusBarAndBackgroundProps = { function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBackgroundProps) { const {isRootStatusBarDisabled, disableRootStatusBar} = useContext(CustomStatusBarAndBackgroundContext); const theme = useTheme(); - const [statusBarStyle, setStatusBarStyle] = useState(theme.statusBarStyle); + const [statusBarStyle, setStatusBarStyle] = useState(); const isDisabled = !isNested && isRootStatusBarDisabled; @@ -34,6 +35,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack }; }, [disableRootStatusBar, isNested]); + const didForceUpdateStatusBarRef = useRef(false); + const prevIsRootStatusBarDisabled = useRef(isRootStatusBarDisabled); const prevStatusBarBackgroundColor = useRef(theme.appBG); const statusBarBackgroundColor = useRef(theme.appBG); const statusBarAnimation = useSharedValue(0); @@ -94,27 +97,40 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack } // Don't update the status bar style if it's the same as the current one, to prevent flashing. - if (newStatusBarStyle !== statusBarStyle) { + // Force update if the root status bar is back on active or it won't overwirte the nested status bar style + if ((!didForceUpdateStatusBarRef.current && prevIsRootStatusBarDisabled.current && !isRootStatusBarDisabled) || newStatusBarStyle !== statusBarStyle) { updateStatusBarAppearance({statusBarStyle: newStatusBarStyle}); setStatusBarStyle(newStatusBarStyle); + + if (prevIsRootStatusBarDisabled.current && !isRootStatusBarDisabled) { + didForceUpdateStatusBarRef.current = true; + } } }, - [statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], + [isRootStatusBarDisabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], ); - // Add navigation state listeners to update the status bar every time the route changes - // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly + useEffect(() => { + prevIsRootStatusBarDisabled.current = isRootStatusBarDisabled; + didForceUpdateStatusBarRef.current = false; + }, [isRootStatusBarDisabled]); + useEffect(() => { if (isDisabled) { return; } + // Update status bar when theme changes + updateStatusBarStyle(); + + // Add navigation state listeners to update the status bar every time the route changes + // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly const listenerId = ++listenerCount.current; const listener = () => updateStatusBarStyle(listenerId); navigationRef.addListener('state', listener); return () => navigationRef.removeListener('state', listener); - }, [isDisabled, theme.appBG, updateStatusBarStyle]); + }, [isDisabled, updateStatusBarStyle]); // Update the global background (on web) everytime the theme changes. // The background of the html element needs to be updated, otherwise you will see a big contrast when resizing the window or when the keyboard is open on iOS web. From 84b4eb44595dbe70a5752cb6e9f044a61975af89 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 17:00:55 +0700 Subject: [PATCH 030/297] use usePrevious --- src/components/CustomStatusBarAndBackground/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 9ca5edeb7ed8..576a7249fe03 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -7,6 +7,7 @@ import {StatusBarStyle} from '@styles/index'; import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext'; import updateGlobalBackgroundColor from './updateGlobalBackgroundColor'; import updateStatusBarAppearance from './updateStatusBarAppearance'; +import usePrevious from '@hooks/usePrevious'; type CustomStatusBarAndBackgroundProps = { /** Whether the CustomStatusBar is nested within another CustomStatusBar. @@ -36,7 +37,7 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack }, [disableRootStatusBar, isNested]); const didForceUpdateStatusBarRef = useRef(false); - const prevIsRootStatusBarDisabled = useRef(isRootStatusBarDisabled); + const prevIsRootStatusBarDisabled = usePrevious(isRootStatusBarDisabled); const prevStatusBarBackgroundColor = useRef(theme.appBG); const statusBarBackgroundColor = useRef(theme.appBG); const statusBarAnimation = useSharedValue(0); @@ -98,11 +99,11 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack // Don't update the status bar style if it's the same as the current one, to prevent flashing. // Force update if the root status bar is back on active or it won't overwirte the nested status bar style - if ((!didForceUpdateStatusBarRef.current && prevIsRootStatusBarDisabled.current && !isRootStatusBarDisabled) || newStatusBarStyle !== statusBarStyle) { + if ((!didForceUpdateStatusBarRef.current && prevIsRootStatusBarDisabled && !isRootStatusBarDisabled) || newStatusBarStyle !== statusBarStyle) { updateStatusBarAppearance({statusBarStyle: newStatusBarStyle}); setStatusBarStyle(newStatusBarStyle); - if (prevIsRootStatusBarDisabled.current && !isRootStatusBarDisabled) { + if (prevIsRootStatusBarDisabled && !isRootStatusBarDisabled) { didForceUpdateStatusBarRef.current = true; } } @@ -111,7 +112,6 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack ); useEffect(() => { - prevIsRootStatusBarDisabled.current = isRootStatusBarDisabled; didForceUpdateStatusBarRef.current = false; }, [isRootStatusBarDisabled]); From acfc8233be9c2382d77a7aa3bd4da6371deca33e Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jan 2024 17:06:24 +0700 Subject: [PATCH 031/297] fix lint --- src/components/CustomStatusBarAndBackground/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 576a7249fe03..fa1ab2362151 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -1,13 +1,13 @@ import React, {useCallback, useContext, useEffect, useRef, useState} from 'react'; import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; +import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import {navigationRef} from '@libs/Navigation/Navigation'; import StatusBar from '@libs/StatusBar'; -import {StatusBarStyle} from '@styles/index'; +import type {StatusBarStyle} from '@styles/index'; import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext'; import updateGlobalBackgroundColor from './updateGlobalBackgroundColor'; import updateStatusBarAppearance from './updateStatusBarAppearance'; -import usePrevious from '@hooks/usePrevious'; type CustomStatusBarAndBackgroundProps = { /** Whether the CustomStatusBar is nested within another CustomStatusBar. @@ -108,7 +108,7 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack } } }, - [isRootStatusBarDisabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], + [prevIsRootStatusBarDisabled, isRootStatusBarDisabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], ); useEffect(() => { From fab9c8e6ed313751ec28fcd8bb15b8aeba0ccc1d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 17 Jan 2024 15:37:20 +0100 Subject: [PATCH 032/297] fix: wip --- src/libs/actions/EmojiPickerAction.ts | 2 +- src/libs/actions/User.ts | 6 +- ...tionCompose.js => ReportActionCompose.tsx} | 153 ++++++++++-------- 3 files changed, 88 insertions(+), 73 deletions(-) rename src/pages/home/report/ReportActionCompose/{ReportActionCompose.js => ReportActionCompose.tsx} (81%) diff --git a/src/libs/actions/EmojiPickerAction.ts b/src/libs/actions/EmojiPickerAction.ts index 56a5f34c0b8e..3a4da86dcbb9 100644 --- a/src/libs/actions/EmojiPickerAction.ts +++ b/src/libs/actions/EmojiPickerAction.ts @@ -66,7 +66,7 @@ function showEmojiPicker( /** * Hide the Emoji Picker modal. */ -function hideEmojiPicker(isNavigating: boolean) { +function hideEmojiPicker(isNavigating?: boolean) { if (!emojiPickerRef.current) { return; } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8e3bd5f2c017..e0f3003ed9e8 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -13,7 +13,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {FrequentlyUsedEmoji} from '@src/types/onyx'; +import type {BlockedFromConcierge, FrequentlyUsedEmoji} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; @@ -27,8 +27,6 @@ import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; -type BlockedFromConciergeNVP = {expiresAt: number}; - let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ @@ -445,7 +443,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * and if so whether the expiresAt date of a user's ban is before right now * */ -function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { +function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { if (isEmptyObject(blockedFromConciergeNVP)) { return false; } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx similarity index 81% rename from src/pages/home/report/ReportActionCompose/ReportActionCompose.js rename to src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index c072666920ae..1f0574f577cb 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,21 +1,23 @@ import {PortalHost} from '@gorhom/portal'; import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {MeasureInWindowOnSuccessCallback, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; -import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {usePersonalDetails, withNetwork} from '@components/OnyxProvider'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import {usePersonalDetails} from '@components/OnyxProvider'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useDebounce from '@hooks/useDebounce'; import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -26,69 +28,88 @@ import getModalState from '@libs/getModalState'; import * as ReportUtils from '@libs/ReportUtils'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import ReportDropUI from '@pages/home/report/ReportDropUI'; import ReportTypingIndicator from '@pages/home/report/ReportTypingIndicator'; -import reportPropTypes from '@pages/reportPropTypes'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as Report from '@userActions/Report'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems'; import ComposerWithSuggestions from './ComposerWithSuggestions'; import SendButton from './SendButton'; -const propTypes = { +type ComposerRef = { + blur: () => void; + focus: (shouldDelay?: boolean) => void; + replaceSelectionWithText: (text: string, shouldAddTrailSpace: boolean) => void; + prepareCommentAndResetComposer: () => string; + isFocused: () => boolean; +}; + +type SuggestionsRef = { + resetSuggestions: () => void; + onSelectionChange: (event: any) => void; + triggerHotkeyActions: (event: any) => void; + updateShouldShowSuggestionMenuToFalse: () => void; + setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; + getSuggestions: () => string[]; +}; + +type ReportActionComposeOnyxProps = { + /** The NVP describing a user's block status */ + blockedFromConcierge: OnyxEntry; + + /** Whether the composer input should be shown */ + shouldShowComposeInput: OnyxEntry; +}; + +type ReportActionComposeProps = { /** A method to call when the form is submitted */ - onSubmit: PropTypes.func.isRequired, + onSubmit: (newComment: string | undefined) => void; /** The ID of the report actions will be created for */ - reportID: PropTypes.string.isRequired, + reportID: string; /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + reportActions?: OnyxTypes.ReportAction[]; /** The report currently being looked at */ - report: reportPropTypes, + report: OnyxEntry; /** Is composer full size */ - isComposerFullSize: PropTypes.bool, + isComposerFullSize?: boolean; /** Whether user interactions should be disabled */ - disabled: PropTypes.bool, + disabled?: boolean; /** Height of the list which the composer is part of */ - listHeight: PropTypes.number, - - // The NVP describing a user's block status - blockedFromConcierge: PropTypes.shape({ - // The date that the user will be unblocked - expiresAt: PropTypes.string, - }), + listHeight?: number; /** Whether the composer input should be shown */ - shouldShowComposeInput: PropTypes.bool, + shouldShowComposeInput?: boolean; /** The type of action that's pending */ - pendingAction: PropTypes.oneOf(['add', 'update', 'delete']), + pendingAction?: OnyxCommon.PendingAction; /** /** Whetjer the report is ready for display */ - isReportReadyForDisplay: PropTypes.bool, - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - report: {}, - blockedFromConcierge: {}, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - isComposerFullSize: false, - pendingAction: null, - shouldShowComposeInput: true, - listHeight: 0, - isReportReadyForDisplay: true, - ...withCurrentUserPersonalDetailsDefaultProps, -}; + isReportReadyForDisplay?: boolean; +} & ReportActionComposeOnyxProps & + WithCurrentUserPersonalDetailsProps; + +// const defaultProps = { +// report: {}, +// blockedFromConcierge: {}, +// preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, +// isComposerFullSize: false, +// pendingAction: null, +// shouldShowComposeInput: true, +// listHeight: 0, +// isReportReadyForDisplay: true, +// ...withCurrentUserPersonalDetailsDefaultProps, +// }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus on existing chat for mobile device @@ -101,7 +122,6 @@ function ReportActionCompose({ currentUserPersonalDetails, disabled, isComposerFullSize, - network, onSubmit, pendingAction, report, @@ -110,10 +130,11 @@ function ReportActionCompose({ listHeight, shouldShowComposeInput, isReportReadyForDisplay, -}) { +}: ReportActionComposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); + const {isOffline} = useNetwork(); const animatedRef = useAnimatedRef(); const actionButtonRef = useRef(null); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; @@ -122,7 +143,7 @@ function ReportActionCompose({ */ const [isFocused, setIsFocused] = useState(() => { const initialModalState = getModalState(); - return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState.isVisible && !initialModalState.willAlertModalBecomeVisible; + return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; }); const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize); @@ -167,11 +188,11 @@ function ReportActionCompose({ */ const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); - const suggestionsRef = useRef(null); - const composerRef = useRef(null); + const suggestionsRef = useRef(null); + const composerRef = useRef(null); const reportParticipantIDs = useMemo( - () => _.without(lodashGet(report, 'participantAccountIDs', []), currentUserPersonalDetails.accountID), + () => report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserPersonalDetails.accountID), [currentUserPersonalDetails.accountID, report], ); @@ -184,7 +205,7 @@ function ReportActionCompose({ // If we are on a small width device then don't show last 3 items from conciergePlaceholderOptions const conciergePlaceholderRandomIndex = useMemo( - () => _.random(translate('reportActionCompose.conciergePlaceholderOptions').length - (isSmallScreenWidth ? 4 : 1)), + () => Math.floor(Math.random() * (translate('reportActionCompose.conciergePlaceholderOptions').length - (isSmallScreenWidth ? 4 : 1) + 1)), // eslint-disable-next-line react-hooks/exhaustive-deps [], ); @@ -203,7 +224,7 @@ function ReportActionCompose({ }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); const focus = () => { - if (composerRef === null || composerRef.current === null) { + if (composerRef?.current === null) { return; } composerRef.current.focus(true); @@ -219,9 +240,9 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = false; }, []); - const containerRef = useRef(null); + const containerRef = useRef(null); const measureContainer = useCallback( - (callback) => { + (callback: MeasureInWindowOnSuccessCallback) => { if (!containerRef.current) { return; } @@ -234,9 +255,9 @@ function ReportActionCompose({ const onAddActionPressed = useCallback(() => { if (!willBlurTextInputOnTapOutside) { - isKeyboardVisibleWhenShowingModalRef.current = composerRef.current.isFocused(); + isKeyboardVisibleWhenShowingModalRef.current = !!composerRef.current?.isFocused(); } - composerRef.current.blur(); + composerRef.current?.blur(); }, []); const onItemSelected = useCallback(() => { @@ -250,12 +271,9 @@ function ReportActionCompose({ suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(false); }, []); - /** - * @param {Object} file - */ const addAttachment = useCallback( - (file) => { - const newComment = composerRef.current.prepareCommentAndResetComposer(); + (file: File) => { + const newComment = composerRef.current?.prepareCommentAndResetComposer(); Report.addAttachment(reportID, file, newComment); setTextInputShouldClear(false); }, @@ -273,16 +291,14 @@ function ReportActionCompose({ /** * Add a new comment to this chat - * - * @param {SyntheticEvent} [e] */ const submitForm = useCallback( - (e) => { + (e?: SyntheticEvent) => { if (e) { e.preventDefault(); } - const newComment = composerRef.current.prepareCommentAndResetComposer(); + const newComment = composerRef.current?.prepareCommentAndResetComposer(); if (!newComment) { return; } @@ -323,7 +339,7 @@ function ReportActionCompose({ // We are returning a callback here as we want to incoke the method on unmount only useEffect( () => () => { - if (!EmojiPickerActions.isActive(report.reportID)) { + if (!EmojiPickerActions.isActive(report?.reportID ?? '')) { return; } EmojiPickerActions.hideEmojiPicker(); @@ -338,7 +354,7 @@ function ReportActionCompose({ const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); - const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || disabled || hasExceededMaxCommentLength; + const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; const handleSendMessage = useCallback(() => { 'worklet'; @@ -355,7 +371,7 @@ function ReportActionCompose({ }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); return ( - + {shouldShowReportRecipientLocalTime && hasReportRecipient && } @@ -376,12 +392,14 @@ function ReportActionCompose({ hasExceededMaxCommentLength && styles.borderColorDanger, ]} > + {/* @ts-expect-error TODO: Remove this once AttachmentModal (https://github.com/Expensify/App/issues/25130) is migrated to TypeScript. */} setIsAttachmentPreviewActive(true)} onModalHide={onAttachmentPreviewClose} > + {/* @ts-expect-error TODO: Remove this once AttachmentModal (https://github.com/Expensify/App/issues/25130) is migrated to TypeScript. */} {({displayFileInModal}) => ( <> composerRef.current.replaceSelectionWithText(...args)} - emojiPickerID={report.reportID} + // @ts-expect-error TODO: Remove this once EmojiPickerButton is migrated to TypeScript. + onEmojiSelected={(...args) => composerRef.current?.replaceSelectionWithText(...args)} + emojiPickerID={report?.reportID} /> )} {!isSmallScreenWidth && } @@ -478,12 +498,9 @@ function ReportActionCompose({ ); } -ReportActionCompose.propTypes = propTypes; -ReportActionCompose.defaultProps = defaultProps; ReportActionCompose.displayName = 'ReportActionCompose'; export default compose( - withNetwork(), withCurrentUserPersonalDetails, withOnyx({ blockedFromConcierge: { From 544e11833abb07788808bf86a02315879867c8da Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 12:35:36 +0800 Subject: [PATCH 033/297] remove promise chaining --- src/libs/actions/IOU.js | 3 +-- src/pages/iou/request/step/IOURequestStepParticipants.js | 9 +-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 4a5f5bb01bfb..fe173a448793 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -270,10 +270,9 @@ function setMoneyRequestBillable_temporaryForRefactor(transactionID, billable) { /** * @param {String} transactionID * @param {Object[]} participants - * @returns {Promise} */ function setMoneyRequestParticipants_temporaryForRefactor(transactionID, participants) { - return Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } /** diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 1d35f7b7a421..83f2082bdb63 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -66,14 +66,7 @@ function IOURequestStepParticipants({ newIouType = CONST.IOU.TYPE.REQUEST; } - IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val).then(() => { - if (!newIouType) { - return; - } - // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before - // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. - IOU.updateMoneyRequestTypeParams(routes, newIouType); - }); + IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); numberOfParticipants.current = val.length; // When multiple participants are selected, the reportID is generated at the end of the confirmation step. From b3eca3db124f455cbaf2b7a2702d571ec8250655 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 12:38:40 +0800 Subject: [PATCH 034/297] update route params after participant is updated --- .../step/IOURequestStepParticipants.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 83f2082bdb63..21ed1a76c141 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -45,6 +45,7 @@ function IOURequestStepParticipants({ const headerTitle = isSplitRequest ? translate('iou.split') : translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); + const newIouType = useRef(); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then @@ -53,17 +54,29 @@ function IOURequestStepParticipants({ IOUUtils.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID); }, [receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]); + const updateRouteParams = useCallback(() => { + IOU.updateMoneyRequestTypeParams(routes, newIouType.current); + }, [routes]); + + useEffect(() => { + if (newIouType.current) { + // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before + // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. + updateRouteParams(); + newIouType.current = null; + } + }, [participants, routes, updateRouteParams]); + const addParticipant = useCallback( (val, isSplit) => { - let newIouType; // It's only possible to switch between REQUEST and SPLIT. // We want to update the IOU type only if it's not updated yet to prevent unnecessary updates. if (isSplit && iouType !== CONST.IOU.TYPE.SPLIT) { - newIouType = CONST.IOU.TYPE.SPLIT; + newIouType.current = CONST.IOU.TYPE.SPLIT; } else if (!isSplit && iouType === CONST.IOU.TYPE.SPLIT) { // Non-split can be either REQUEST or SEND. Instead of checking whether // the current IOU type is not a REQUEST (true for SEND), we check whether the current IOU type is a SPLIT. - newIouType = CONST.IOU.TYPE.REQUEST; + newIouType.current = CONST.IOU.TYPE.REQUEST; } IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); From df2089845783a77b3c5943c41f0cca1bdc2ba2c7 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 12:46:26 +0800 Subject: [PATCH 035/297] update route params immediately if participants are the same --- src/pages/iou/request/step/IOURequestStepParticipants.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 21ed1a76c141..497370ebcfb5 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -79,6 +79,13 @@ function IOURequestStepParticipants({ newIouType.current = CONST.IOU.TYPE.REQUEST; } + // If the Onyx participants has the same items as the selected participants (val), Onyx won't update it + // thus this component won't rerender, so we can immediately update the route params. + if (newIouType.current && _.isEqual(participants, val)) { + updateRouteParams(); + newIouType.current = null; + } + IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); numberOfParticipants.current = val.length; From dfe25cb60deb336a9e879fd0a98b1990ebfc5e1a Mon Sep 17 00:00:00 2001 From: Sheena Trepanier Date: Wed, 17 Jan 2024 21:06:57 -0800 Subject: [PATCH 036/297] Update Admin-Card-Settings-and-Features.md Update terminology for "credit card" to satisfy Card Management updates as part of https://github.com/Expensify/Expensify/issues/342533 --- .../expensify-card/Admin-Card-Settings-and-Features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md index 16e628acbeee..043cc4be1e26 100644 --- a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md @@ -81,7 +81,7 @@ Follow the below steps to run reconciliation on the Expensify Card settlements: - Entry ID: a unique number grouping card payments and transactions settled by those payments - Amount: the amount debited from the Business Bank Account for payments - Merchant: the business where a purchase was made - - Card: refers to the Expensify credit card number and cardholder's email address + - Card: refers to the Expensify Card number and cardholder's email address - Business Account: the business bank account connected to Expensify that the settlement is paid from - Transaction ID: a special ID that helps Expensify support locate transactions if there's an issue From 5ad0cbacbc654614356e3e5287501a3d6c6a6392 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 13:21:20 +0800 Subject: [PATCH 037/297] replace useNavigationState hook --- src/pages/iou/request/IOURequestStartPage.js | 12 +++++----- .../step/IOURequestStepParticipants.js | 24 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 89b2bfd43393..e6f98ae4dab2 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -1,4 +1,4 @@ -import {useNavigationState} from '@react-navigation/native'; +import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; @@ -70,7 +70,7 @@ function IOURequestStartPage({ }) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const routes = useNavigationState((state) => state.routes); + const navigation = useNavigation(); const [isDraggingOver, setIsDraggingOver] = useState(false); const tabTitles = { [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), @@ -114,12 +114,12 @@ function IOURequestStartPage({ return; } if (iouType === CONST.IOU.TYPE.SPLIT) { - IOU.updateMoneyRequestTypeParams(routes, CONST.IOU.TYPE.REQUEST); + IOU.updateMoneyRequestTypeParams(navigation.getState().routes, CONST.IOU.TYPE.REQUEST); } IOU.startMoneyRequest_temporaryForRefactor(reportID, isFromGlobalCreate, newIouType); transactionRequestType.current = newIouType; }, - [previousIOURequestType, reportID, isFromGlobalCreate, iouType, routes], + [previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation], ); if (!transaction.transactionID) { @@ -151,10 +151,10 @@ function IOURequestStartPage({ id={CONST.TAB.IOU_REQUEST_TYPE} selectedTab={selectedTab || CONST.IOU.REQUEST_TYPE.SCAN} onTabSelected={resetIOUTypeIfChanged} - tabBar={({state, navigation, position}) => ( + tabBar={({state, navigation: tabNavigation, position}) => ( )} diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 497370ebcfb5..d11fcc150076 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -1,6 +1,7 @@ -import {useNavigationState} from '@react-navigation/native'; +import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef} from 'react'; +import _ from 'underscore'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; @@ -37,7 +38,7 @@ function IOURequestStepParticipants({ transaction: {participants = []}, }) { const {translate} = useLocalize(); - const routes = useNavigationState((state) => state.routes); + const navigation = useNavigation(); const selectedReportID = useRef(reportID); const numberOfParticipants = useRef(participants.length); const iouRequestType = TransactionUtils.getRequestType(transaction); @@ -55,17 +56,18 @@ function IOURequestStepParticipants({ }, [receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]); const updateRouteParams = useCallback(() => { - IOU.updateMoneyRequestTypeParams(routes, newIouType.current); - }, [routes]); + IOU.updateMoneyRequestTypeParams(navigation.getState().routes, newIouType.current); + }, [navigation]); useEffect(() => { - if (newIouType.current) { - // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before - // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. - updateRouteParams(); - newIouType.current = null; + if (!newIouType.current) { + return; } - }, [participants, routes, updateRouteParams]); + // Participants can be added as normal or split participants. We want to wait for the participants' data to be updated before + // updating the money request type route params reducing the overhead of the thread and preventing possible jitters in UI. + updateRouteParams(); + newIouType.current = null; + }, [participants, updateRouteParams]); const addParticipant = useCallback( (val, isSplit) => { @@ -99,7 +101,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = lodashGet(val, '[0].reportID', reportID); }, - [reportID, transactionID, iouType, routes], + [reportID, transactionID, iouType, participants, updateRouteParams], ); const goToNextStep = useCallback( From 20b9724989b5a6e59186a8a87e6138db8df6c4e3 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 18 Jan 2024 15:39:48 +0800 Subject: [PATCH 038/297] fix money request tab navigator params are not updated properly --- src/libs/actions/IOU.js | 16 +++++++++++++--- src/pages/iou/request/IOURequestStartPage.js | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index fe173a448793..862f9f43d1b6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -178,13 +178,23 @@ function clearMoneyRequest(transactionID) { * * @param {Object[]} routes * @param {String} newIouType + * @param {String} tab */ -function updateMoneyRequestTypeParams(routes, newIouType) { +function updateMoneyRequestTypeParams(routes, newIouType, tab) { routes.forEach((route) => { - if (!route.name.startsWith('Money_Request_')) { + if (!route.name.startsWith('Money_Request_') && ![CONST.TAB_REQUEST.DISTANCE, CONST.TAB_REQUEST.MANUAL, CONST.TAB_REQUEST.SCAN].includes(route.name)) { return; } - Navigation.setParams({iouType: newIouType}, route.key); + const newParams = {iouType: newIouType}; + if (route.name === 'Money_Request_Create') { + // Both screen and nested params are needed to properly update the nested tab navigator + newParams.params = {...newParams}; + newParams.screen = tab; + } + Navigation.setParams(newParams, route.key); + + // Recursively update nested money request tab params + updateMoneyRequestTypeParams(lodashGet(route, 'state.routes', []), newIouType, tab); }); } diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index e6f98ae4dab2..c057c4d8a6e1 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -114,7 +114,7 @@ function IOURequestStartPage({ return; } if (iouType === CONST.IOU.TYPE.SPLIT) { - IOU.updateMoneyRequestTypeParams(navigation.getState().routes, CONST.IOU.TYPE.REQUEST); + IOU.updateMoneyRequestTypeParams(navigation.getState().routes, CONST.IOU.TYPE.REQUEST, newIouType); } IOU.startMoneyRequest_temporaryForRefactor(reportID, isFromGlobalCreate, newIouType); transactionRequestType.current = newIouType; From 25f6646d0486eb0ce45714956e7118c65281ef31 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 18 Jan 2024 10:46:39 +0100 Subject: [PATCH 039/297] fix: finish working on migration --- .../ReportActionCompose.tsx | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 1f0574f577cb..2cdb4e4d3602 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,8 +1,8 @@ import {PortalHost} from '@gorhom/portal'; -import lodashGet from 'lodash/get'; import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {MeasureInWindowOnSuccessCallback, View} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; @@ -21,7 +21,6 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import compose from '@libs/compose'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getModalState from '@libs/getModalState'; @@ -37,6 +36,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems'; import ComposerWithSuggestions from './ComposerWithSuggestions'; import SendButton from './SendButton'; @@ -51,9 +51,9 @@ type ComposerRef = { type SuggestionsRef = { resetSuggestions: () => void; - onSelectionChange: (event: any) => void; - triggerHotkeyActions: (event: any) => void; - updateShouldShowSuggestionMenuToFalse: () => void; + onSelectionChange: (event: NativeSyntheticEvent) => void; + triggerHotkeyActions: (event: KeyboardEvent) => void; + updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; getSuggestions: () => string[]; }; @@ -99,18 +99,6 @@ type ReportActionComposeProps = { } & ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps; -// const defaultProps = { -// report: {}, -// blockedFromConcierge: {}, -// preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, -// isComposerFullSize: false, -// pendingAction: null, -// shouldShowComposeInput: true, -// listHeight: 0, -// isReportReadyForDisplay: true, -// ...withCurrentUserPersonalDetailsDefaultProps, -// }; - // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus on existing chat for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); @@ -119,17 +107,17 @@ const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionCompose({ blockedFromConcierge, - currentUserPersonalDetails, + currentUserPersonalDetails = {}, disabled, - isComposerFullSize, + isComposerFullSize = false, onSubmit, pendingAction, report, reportID, reportActions, - listHeight, - shouldShowComposeInput, - isReportReadyForDisplay, + listHeight = 0, + shouldShowComposeInput = true, + isReportReadyForDisplay = true, }: ReportActionComposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -313,7 +301,7 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((e) => { + const onBlur = useCallback((e: FocusEvent) => { setIsFocused(false); if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); @@ -352,7 +340,7 @@ function ReportActionCompose({ const reportRecipient = personalDetails[reportRecipientAcountIDs[0]]; const shouldUseFocusedColor = !isBlockedFromConcierge && !disabled && isFocused; - const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); + const hasReportRecipient = !isEmptyObject(reportRecipient); const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; @@ -403,6 +391,7 @@ function ReportActionCompose({ {({displayFileInModal}) => ( <> { + onDrop={(e: DragEvent) => { if (isAttachmentPreviewActive) { return; } - const data = lodashGet(e, ['dataTransfer', 'items', 0]); + const data = e.dataTransfer?.items[0]; displayFileInModal(data); }} /> @@ -500,14 +489,13 @@ function ReportActionCompose({ ReportActionCompose.displayName = 'ReportActionCompose'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ blockedFromConcierge: { key: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, }, shouldShowComposeInput: { key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, }, - }), -)(ReportActionCompose); + })(ReportActionCompose), +); From 032e9e32f529ab28001a5db554460cbfd3125fa4 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 18 Jan 2024 17:01:26 +0700 Subject: [PATCH 040/297] display correct tooltip for the case subscription --- src/components/MultipleAvatars.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 48a9bd77c09e..5b639d832dce 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -107,7 +107,6 @@ function MultipleAvatars({ let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction); const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size, avatarSizeToStylesMap]); - const tooltipTexts = useMemo(() => (shouldShowTooltip ? icons.map((icon) => icon.name) : ['']), [shouldShowTooltip, icons]); const avatarSize = useMemo(() => { if (isFocusMode) { return CONST.AVATAR_SIZE.MID_SUBSCRIPT; @@ -215,9 +214,13 @@ function MultipleAvatars({ {avatars.length > maxAvatarsInRow && ( Number(icon.id)), - )} + text={ + shouldShowTooltip + ? ReportUtils.getUserDetailsTooltipText( + icons.slice(avatarRows.length * maxAvatarsInRow - 1, avatarRows.length * maxAvatarsInRow + 9).map((icon) => Number(icon.id)), + ) + : '' + } > ) : ( - + Number(icon.id))) : ''}> Date: Thu, 18 Jan 2024 13:40:22 +0100 Subject: [PATCH 041/297] fix: typecheck --- src/components/ButtonWithDropdownMenu.tsx | 4 ++-- src/components/MoneyRequestConfirmationList.tsx | 4 ++-- src/components/PopoverMenu.tsx | 2 +- src/libs/DistanceRequestUtils.ts | 12 +++--------- src/types/onyx/IOU.ts | 2 ++ src/types/onyx/Policy.ts | 2 +- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu.tsx b/src/components/ButtonWithDropdownMenu.tsx index 466c68229a32..fc33c373ad45 100644 --- a/src/components/ButtonWithDropdownMenu.tsx +++ b/src/components/ButtonWithDropdownMenu.tsx @@ -19,7 +19,7 @@ import PopoverMenu from './PopoverMenu'; type DropdownOption = { value: string; text: string; - icon: IconAsset; + icon?: IconAsset; iconWidth?: number; iconHeight?: number; iconDescription?: string; @@ -55,7 +55,7 @@ type ButtonWithDropdownMenuProps = { anchorAlignment?: AnchorAlignment; /* ref for the button */ - buttonRef: RefObject; + buttonRef?: RefObject; }; function ButtonWithDropdownMenu({ diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index f1d042b29cad..3712e928a5e1 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -20,7 +20,6 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import type {Participant} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -28,6 +27,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentType} from '@src/types/onyx/OriginalMessage'; import type {MileageRate} from '@src/types/onyx/Policy'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; @@ -552,7 +552,7 @@ function MoneyRequestConfirmationList({ pressOnEnter isDisabled={shouldDisableButton} // eslint-disable-next-line @typescript-eslint/naming-convention - onPress={(_, value) => confirm(value)} + onPress={(_, value) => confirm(value as PaymentType)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} /> diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 17b1a119671a..ae995c899521 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -17,7 +17,7 @@ import Text from './Text'; type PopoverMenuItem = { /** An icon element displayed on the left side */ - icon: IconAsset; + icon?: IconAsset; /** Text label */ text: string; diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c92e9bfd3f67..6c7444ea12c7 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -1,17 +1,11 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; -import type {Unit} from '@src/types/onyx/Policy'; +import type {MileageRate, Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; -type DefaultMileageRate = { - rate?: number; - currency?: string; - unit: Unit; -}; - /** * Retrieves the default mileage rate based on a given policy. * @@ -22,7 +16,7 @@ type DefaultMileageRate = { * @returns [currency] - The currency associated with the rate. * @returns [unit] - The unit of measurement for the distance. */ -function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | null { +function getDefaultMileageRate(policy: OnyxEntry): MileageRate | null { if (!policy?.customUnits) { return null; } @@ -39,7 +33,7 @@ function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | return { rate: distanceRate.rate, - currency: distanceRate.currency, + currency: distanceRate.currency ?? 'USD', unit: distanceUnit.attributes.unit, }; } diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index a89b0d4530ef..220af7005c45 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -23,3 +23,5 @@ type IOU = { }; export default IOU; + +export type {Participant}; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ee8c69fa3f21..e5681e53dc15 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -19,7 +19,7 @@ type Attributes = { type MileageRate = { unit: Unit; - rate: number; + rate?: number; currency: string; }; From 5e2f31c82fda91671e4c4c1e601a7755335fb67f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 19 Jan 2024 11:33:28 +0100 Subject: [PATCH 042/297] fix: resolve comments --- .../ReportActionCompose.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 2cdb4e4d3602..73b025c50df2 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -88,13 +88,10 @@ type ReportActionComposeProps = { /** Height of the list which the composer is part of */ listHeight?: number; - /** Whether the composer input should be shown */ - shouldShowComposeInput?: boolean; - /** The type of action that's pending */ pendingAction?: OnyxCommon.PendingAction; - /** /** Whetjer the report is ready for display */ + /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; } & ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps; @@ -212,7 +209,7 @@ function ReportActionCompose({ }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); const focus = () => { - if (composerRef?.current === null) { + if (composerRef.current === null) { return; } composerRef.current.focus(true); @@ -281,9 +278,9 @@ function ReportActionCompose({ * Add a new comment to this chat */ const submitForm = useCallback( - (e?: SyntheticEvent) => { - if (e) { - e.preventDefault(); + (event?: SyntheticEvent) => { + if (event) { + event.preventDefault(); } const newComment = composerRef.current?.prepareCommentAndResetComposer(); @@ -301,12 +298,12 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((e: FocusEvent) => { + const onBlur = useCallback((event: FocusEvent) => { setIsFocused(false); if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); } - if (e.relatedTarget && e.relatedTarget === actionButtonRef.current) { + if (event.relatedTarget && event.relatedTarget === actionButtonRef.current) { isKeyboardVisibleWhenShowingModalRef.current = true; } }, []); @@ -444,11 +441,11 @@ function ReportActionCompose({ onValueChange={validateCommentMaxLength} /> { + onDrop={(event: DragEvent) => { if (isAttachmentPreviewActive) { return; } - const data = e.dataTransfer?.items[0]; + const data = event.dataTransfer?.items[0]; displayFileInModal(data); }} /> @@ -459,7 +456,7 @@ function ReportActionCompose({ composerRef.current?.replaceSelectionWithText(...args)} emojiPickerID={report?.reportID} /> From 2a0e3e21c0b61a5f9409137e29986e4d14c93bac Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 19 Jan 2024 15:14:18 +0100 Subject: [PATCH 043/297] ref: move SuggestionMention, Suggestions to TS, fixes to MentionSuggestions and ReportActionCompose --- src/components/MentionSuggestions.tsx | 6 +- .../ReportActionCompose.tsx | 9 +- ...SuggestionEmoji.js => SuggestionEmoji.tsx} | 84 ++++------ ...estionMention.js => SuggestionMention.tsx} | 145 ++++++++---------- .../{Suggestions.js => Suggestions.tsx} | 119 +++++++------- src/types/onyx/OnyxCommon.ts | 2 +- 6 files changed, 159 insertions(+), 206 deletions(-) rename src/pages/home/report/ReportActionCompose/{SuggestionEmoji.js => SuggestionEmoji.tsx} (86%) rename src/pages/home/report/ReportActionCompose/{SuggestionMention.js => SuggestionMention.tsx} (73%) rename src/pages/home/report/ReportActionCompose/{Suggestions.js => Suggestions.tsx} (54%) diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 459131ecc434..99930f995a3a 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -18,7 +18,7 @@ type Mention = { alternateText: string; /** Email/phone number of the user */ - login: string; + login?: string; /** Array of icons of the user. We use the first element of this array */ icons: Icon[]; @@ -32,7 +32,7 @@ type MentionSuggestionsProps = { mentions: Mention[]; /** Fired when the user selects a mention */ - onSelect: () => void; + onSelect: (highlightedMentionIndex: number) => void; /** Mention prefix that follows the @ sign */ prefix: string; @@ -142,3 +142,5 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe MentionSuggestions.displayName = 'MentionSuggestions'; export default MentionSuggestions; + +export type {Mention}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 73b025c50df2..025b7142df0d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -9,6 +9,7 @@ import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; +import type {Mention} from '@components/MentionSuggestions'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -51,11 +52,11 @@ type ComposerRef = { type SuggestionsRef = { resetSuggestions: () => void; - onSelectionChange: (event: NativeSyntheticEvent) => void; + onSelectionChange?: (event: NativeSyntheticEvent) => void; triggerHotkeyActions: (event: KeyboardEvent) => void; - updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu: boolean) => void; + updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; - getSuggestions: () => string[]; + getSuggestions: () => Mention[]; }; type ReportActionComposeOnyxProps = { @@ -496,3 +497,5 @@ export default withCurrentUserPersonalDetails( }, })(ReportActionCompose), ); + +export type {SuggestionsRef}; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx similarity index 86% rename from src/pages/home/report/ReportActionCompose/SuggestionEmoji.js rename to src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index e35d1e90bd5a..06089b748554 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import EmojiSuggestions from '@components/EmojiSuggestions'; @@ -9,17 +10,15 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import * as SuggestionProps from './suggestionProps'; +// eslint-disable-next-line import/no-cycle +import {SuggestionProps} from './Suggestions'; /** * Check if this piece of string looks like an emoji - * @param {String} str - * @param {Number} pos - * @returns {Boolean} */ -const isEmojiCode = (str, pos) => { +const isEmojiCode = (str: string, pos: number): boolean => { const leftWords = str.slice(0, pos).split(CONST.REGEX.SPECIAL_CHAR_OR_EMOJI); - const leftWord = _.last(leftWords); + const leftWord = leftWords.at(-1) ?? ''; return CONST.REGEX.HAS_COLON_ONLY_AT_THE_BEGINNING.test(leftWord) && leftWord.length > 2; }; @@ -29,38 +28,33 @@ const defaultSuggestionsValues = { shouldShowSuggestionMenu: false, }; -const propTypes = { +type SuggestionEmojiOnyxProps = { /** Preferred skin tone */ - preferredSkinTone: PropTypes.number, - - /** A ref to this component */ - forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), - - /** Function to clear the input */ - resetKeyboardInput: PropTypes.func.isRequired, - - ...SuggestionProps.baseProps, + preferredSkinTone: OnyxEntry; }; -const defaultProps = { - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - forwardedRef: null, -}; - -function SuggestionEmoji({ - preferredSkinTone, - value, - setValue, - selection, - setSelection, - updateComment, - isComposerFullSize, - isAutoSuggestionPickerLarge, - forwardedRef, - resetKeyboardInput, - measureParentContainer, - isComposerFocused, -}) { +type SuggestionEmojiProps = { + /** Function to clear the input */ + resetKeyboardInput: () => void; +} & SuggestionEmojiOnyxProps & + SuggestionProps; + +function SuggestionEmoji( + { + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + value, + setValue, + selection, + setSelection, + updateComment, + isComposerFullSize, + isAutoSuggestionPickerLarge, + resetKeyboardInput, + measureParentContainer, + isComposerFocused, + }: SuggestionEmojiProps, + ref: ForwardedRef, +) { const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); const isEmojiSuggestionsMenuVisible = !_.isEmpty(suggestionValues.suggestedEmojis) && suggestionValues.shouldShowSuggestionMenu; @@ -214,7 +208,7 @@ function SuggestionEmoji({ }, []); useImperativeHandle( - forwardedRef, + ref, () => ({ resetSuggestions, onSelectionChange, @@ -248,23 +242,11 @@ function SuggestionEmoji({ ); } -SuggestionEmoji.propTypes = propTypes; -SuggestionEmoji.defaultProps = defaultProps; SuggestionEmoji.displayName = 'SuggestionEmoji'; -const SuggestionEmojiWithRef = React.forwardRef((props, ref) => ( - -)); - -SuggestionEmojiWithRef.displayName = 'SuggestionEmojiWithRef'; - -export default withOnyx({ +export default withOnyx({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, selector: EmojiUtils.getPreferredSkinToneIndex, }, -})(SuggestionEmojiWithRef); +})(forwardRef(SuggestionEmoji)); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx similarity index 73% rename from src/pages/home/report/ReportActionCompose/SuggestionMention.js rename to src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index af3074eec06d..5ce33d916a08 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import _ from 'underscore'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; +import type {Mention} from '@components/MentionSuggestions'; import MentionSuggestions from '@components/MentionSuggestions'; import {usePersonalDetails} from '@components/OnyxProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; @@ -11,52 +11,41 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; -import * as SuggestionProps from './suggestionProps'; +import type {PersonalDetailsList} from '@src/types/onyx'; +import type {SuggestionsRef} from './ReportActionCompose'; +import type {SuggestionProps} from './Suggestions'; + +type SuggestionMentionProps = {isAutoSuggestionPickerLarge: boolean} & SuggestionProps; + +type SuggestionValues = { + suggestedMentions: Mention[]; + atSignIndex: number; + shouldShowSuggestionMenu: boolean; + mentionPrefix: string; +}; /** * Check if this piece of string looks like a mention - * @param {String} str - * @returns {Boolean} */ -const isMentionCode = (str) => CONST.REGEX.HAS_AT_MOST_TWO_AT_SIGNS.test(str); +const isMentionCode = (str: string): boolean => CONST.REGEX.HAS_AT_MOST_TWO_AT_SIGNS.test(str); -const defaultSuggestionsValues = { +const defaultSuggestionsValues: SuggestionValues = { suggestedMentions: [], atSignIndex: -1, shouldShowSuggestionMenu: false, mentionPrefix: '', }; -const propTypes = { - /** A ref to this component */ - forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), - - ...SuggestionProps.implementationBaseProps, -}; - -const defaultProps = { - forwardedRef: null, -}; - -function SuggestionMention({ - value, - setValue, - selection, - setSelection, - isComposerFullSize, - updateComment, - composerHeight, - forwardedRef, - isAutoSuggestionPickerLarge, - measureParentContainer, - isComposerFocused, -}) { - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; +function SuggestionMention( + {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused}: SuggestionMentionProps, + ref: ForwardedRef, +) { + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const {translate, formatPhoneNumber} = useLocalize(); const previousValue = usePrevious(value); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); - const isMentionSuggestionsMenuVisible = !_.isEmpty(suggestionValues.suggestedMentions) && suggestionValues.shouldShowSuggestionMenu; + const isMentionSuggestionsMenuVisible = !!suggestionValues.suggestedMentions.length && suggestionValues.shouldShowSuggestionMenu; const [highlightedMentionIndex, setHighlightedMentionIndex] = useArrowKeyFocusManager({ isActive: isMentionSuggestionsMenuVisible, @@ -69,10 +58,9 @@ function SuggestionMention({ /** * Replace the code of mention and update selection - * @param {Number} highlightedMentionIndex */ const insertSelectedMention = useCallback( - (highlightedMentionIndexInner) => { + (highlightedMentionIndexInner: number) => { const commentBeforeAtSign = value.slice(0, suggestionValues.atSignIndex); const mentionObject = suggestionValues.suggestedMentions[highlightedMentionIndexInner]; const mentionCode = mentionObject.text === CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT ? CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT : `@${mentionObject.login}`; @@ -100,23 +88,21 @@ function SuggestionMention({ /** * Listens for keyboard shortcuts and applies the action - * - * @param {Object} e */ const triggerHotkeyActions = useCallback( - (e) => { + (event: KeyboardEvent) => { const suggestionsExist = suggestionValues.suggestedMentions.length > 0; - if (((!e.shiftKey && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) || e.key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) && suggestionsExist) { - e.preventDefault(); + if (((!event.shiftKey && event.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) || event.key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) && suggestionsExist) { + event.preventDefault(); if (suggestionValues.suggestedMentions.length > 0) { insertSelectedMention(highlightedMentionIndex); return true; } } - if (e.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) { - e.preventDefault(); + if (event.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) { + event.preventDefault(); if (suggestionsExist) { resetSuggestions(); @@ -129,7 +115,7 @@ function SuggestionMention({ ); const getMentionOptions = useCallback( - (personalDetailsParam, searchValue = '') => { + (personalDetailsParam: PersonalDetailsList, searchValue = ''): Mention[] => { const suggestions = []; if (CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT.includes(searchValue.toLowerCase())) { @@ -139,15 +125,15 @@ function SuggestionMention({ icons: [ { source: Expensicons.Megaphone, - type: 'avatar', + type: CONST.ICON_TYPE_AVATAR, }, ], }); } - const filteredPersonalDetails = _.filter(_.values(personalDetailsParam), (detail) => { + const filteredPersonalDetails = Object.values(personalDetailsParam ?? {}).filter((detail) => { // If we don't have user's primary login, that member is not known to the current user and hence we do not allow them to be mentioned - if (!detail.login || detail.isOptimisticPersonalDetail) { + if (!detail?.login || detail.isOptimisticPersonalDetail) { return false; } const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail); @@ -158,18 +144,29 @@ function SuggestionMention({ return true; }); - const sortedPersonalDetails = _.sortBy(filteredPersonalDetails, (detail) => detail.displayName || detail.login); - _.each(_.first(sortedPersonalDetails, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length), (detail) => { + const sortedPersonalDetails = filteredPersonalDetails.sort((a, b) => { + const nameA = a?.displayName ?? a?.login ?? ''; + const nameB = b?.displayName ?? b?.login ?? ''; + + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + return 0; + }); + sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), - alternateText: formatPhoneNumber(detail.login), - login: detail.login, + alternateText: formatPhoneNumber(detail?.login ?? ''), + login: detail?.login, icons: [ { - name: detail.login, - source: UserUtils.getAvatar(detail.avatar, detail.accountID), - type: 'avatar', - fallbackIcon: detail.fallbackIcon, + name: detail?.login, + source: UserUtils.getAvatar(detail?.avatar, detail?.accountID), + type: CONST.ICON_TYPE_AVATAR, + fallbackIcon: detail?.fallbackIcon, }, ], }); @@ -181,7 +178,7 @@ function SuggestionMention({ ); const calculateMentionSuggestion = useCallback( - (selectionEnd) => { + (selectionEnd: number) => { if (shouldBlockCalc.current || selectionEnd < 1 || !isComposerFocused) { shouldBlockCalc.current = false; resetSuggestions(); @@ -201,11 +198,11 @@ function SuggestionMention({ const leftString = value.substring(0, suggestionEndIndex); const words = leftString.split(CONST.REGEX.SPACE_OR_EMOJI); - const lastWord = _.last(words); + const lastWord = words.at(-1) ?? ''; const secondToLastWord = words[words.length - 3]; let atSignIndex; - let suggestionWord; + let suggestionWord = ''; let prefix; // Detect if the last two words contain a mention (two words are needed to detect a mention with a space in it) @@ -223,7 +220,7 @@ function SuggestionMention({ prefix = lastWord.substring(1); } - const nextState = { + const nextState: Partial = { suggestedMentions: [], atSignIndex, mentionPrefix: prefix, @@ -235,7 +232,7 @@ function SuggestionMention({ const suggestions = getMentionOptions(personalDetails, prefix); nextState.suggestedMentions = suggestions; - nextState.shouldShowSuggestionMenu = !_.isEmpty(suggestions); + nextState.shouldShowSuggestionMenu = !!suggestions.length; } setSuggestionValues((prevState) => ({ @@ -268,20 +265,16 @@ function SuggestionMention({ }, []); const setShouldBlockSuggestionCalc = useCallback( - (shouldBlockSuggestionCalc) => { + (shouldBlockSuggestionCalc: boolean) => { shouldBlockCalc.current = shouldBlockSuggestionCalc; }, [shouldBlockCalc], ); - const onClose = useCallback(() => { - setSuggestionValues((prevState) => ({...prevState, suggestedMentions: []})); - }, []); - const getSuggestions = useCallback(() => suggestionValues.suggestedMentions, [suggestionValues]); useImperativeHandle( - forwardedRef, + ref, () => ({ resetSuggestions, triggerHotkeyActions, @@ -298,34 +291,16 @@ function SuggestionMention({ return ( ); } -SuggestionMention.propTypes = propTypes; -SuggestionMention.defaultProps = defaultProps; SuggestionMention.displayName = 'SuggestionMention'; -const SuggestionMentionWithRef = React.forwardRef((props, ref) => ( - -)); - -SuggestionMentionWithRef.displayName = 'SuggestionMentionWithRef'; - -export default SuggestionMentionWithRef; +export default forwardRef(SuggestionMention); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.js b/src/pages/home/report/ReportActionCompose/Suggestions.tsx similarity index 54% rename from src/pages/home/report/ReportActionCompose/Suggestions.js rename to src/pages/home/report/ReportActionCompose/Suggestions.tsx index 5dc71fec6419..f997637a8c4c 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.js +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -1,64 +1,67 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useContext, useEffect, useImperativeHandle, useRef} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef} from 'react'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; import {DragAndDropContext} from '@components/DragAndDrop/Provider'; import usePrevious from '@hooks/usePrevious'; +import type {SuggestionsRef} from './ReportActionCompose'; import SuggestionEmoji from './SuggestionEmoji'; import SuggestionMention from './SuggestionMention'; -import * as SuggestionProps from './suggestionProps'; -const propTypes = { - /** A ref to this component */ - forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), - - /** Function to clear the input */ - resetKeyboardInput: PropTypes.func.isRequired, - - /** Is auto suggestion picker large */ - isAutoSuggestionPickerLarge: PropTypes.bool, - - ...SuggestionProps.baseProps, +type Selection = { + start: number; + end: number; }; -const defaultProps = { - forwardedRef: null, - isAutoSuggestionPickerLarge: true, +type SuggestionProps = { + value: string; + setValue: (newValue: string) => void; + selection: Selection; + setSelection: (newSelection: Selection) => void; + updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; + measureParentContainer: () => void; + isComposerFullSize: boolean; + isComposerFocused?: boolean; + resetKeyboardInput: () => void; + isAutoSuggestionPickerLarge?: boolean; + composerHeight?: number; }; /** * This component contains the individual suggestion components. * If you want to add a new suggestion type, add it here. * - * @returns {React.Component} */ -function Suggestions({ - isComposerFullSize, - value, - setValue, - selection, - setSelection, - updateComment, - composerHeight, - forwardedRef, - resetKeyboardInput, - measureParentContainer, - isAutoSuggestionPickerLarge, - isComposerFocused, -}) { - const suggestionEmojiRef = useRef(null); - const suggestionMentionRef = useRef(null); +function Suggestions( + { + isComposerFullSize, + value, + setValue, + selection, + setSelection, + updateComment, + composerHeight, + resetKeyboardInput, + measureParentContainer, + isAutoSuggestionPickerLarge = true, + isComposerFocused, + }: SuggestionProps, + ref: ForwardedRef, +) { + const suggestionEmojiRef = useRef(null); + const suggestionMentionRef = useRef(null); const {isDraggingOver} = useContext(DragAndDropContext); const prevIsDraggingOver = usePrevious(isDraggingOver); const getSuggestions = useCallback(() => { - if (suggestionEmojiRef.current && suggestionEmojiRef.current.getSuggestions) { + if (suggestionEmojiRef.current?.getSuggestions) { const emojiSuggestions = suggestionEmojiRef.current.getSuggestions(); if (emojiSuggestions.length > 0) { return emojiSuggestions; } } - if (suggestionMentionRef.current && suggestionMentionRef.current.getSuggestions) { + if (suggestionMentionRef.current?.getSuggestions) { const mentionSuggestions = suggestionMentionRef.current.getSuggestions(); if (mentionSuggestions.length > 0) { return mentionSuggestions; @@ -72,38 +75,36 @@ function Suggestions({ * Clean data related to EmojiSuggestions */ const resetSuggestions = useCallback(() => { - suggestionEmojiRef.current.resetSuggestions(); - suggestionMentionRef.current.resetSuggestions(); + suggestionEmojiRef.current?.resetSuggestions(); + suggestionMentionRef.current?.resetSuggestions(); }, []); /** * Listens for keyboard shortcuts and applies the action - * - * @param {Object} e */ - const triggerHotkeyActions = useCallback((e) => { - const emojiHandler = suggestionEmojiRef.current.triggerHotkeyActions(e); - const mentionHandler = suggestionMentionRef.current.triggerHotkeyActions(e); - return emojiHandler || mentionHandler; + const triggerHotkeyActions = useCallback((e: KeyboardEvent) => { + const emojiHandler = suggestionEmojiRef.current?.triggerHotkeyActions(e); + const mentionHandler = suggestionMentionRef.current?.triggerHotkeyActions(e); + return emojiHandler ?? mentionHandler; }, []); - const onSelectionChange = useCallback((e) => { - const emojiHandler = suggestionEmojiRef.current.onSelectionChange(e); + const onSelectionChange = useCallback((e: NativeSyntheticEvent) => { + const emojiHandler = suggestionEmojiRef.current?.onSelectionChange(e); return emojiHandler; }, []); const updateShouldShowSuggestionMenuToFalse = useCallback(() => { - suggestionEmojiRef.current.updateShouldShowSuggestionMenuToFalse(); - suggestionMentionRef.current.updateShouldShowSuggestionMenuToFalse(); + suggestionEmojiRef.current?.updateShouldShowSuggestionMenuToFalse(); + suggestionMentionRef.current?.updateShouldShowSuggestionMenuToFalse(); }, []); - const setShouldBlockSuggestionCalc = useCallback((shouldBlock) => { - suggestionEmojiRef.current.setShouldBlockSuggestionCalc(shouldBlock); - suggestionMentionRef.current.setShouldBlockSuggestionCalc(shouldBlock); + const setShouldBlockSuggestionCalc = useCallback((shouldBlock: boolean) => { + suggestionEmojiRef.current?.setShouldBlockSuggestionCalc(shouldBlock); + suggestionMentionRef.current?.setShouldBlockSuggestionCalc(shouldBlock); }, []); useImperativeHandle( - forwardedRef, + ref, () => ({ resetSuggestions, onSelectionChange, @@ -152,18 +153,8 @@ function Suggestions({ ); } -Suggestions.propTypes = propTypes; -Suggestions.defaultProps = defaultProps; Suggestions.displayName = 'Suggestions'; -const SuggestionsWithRef = React.forwardRef((props, ref) => ( - -)); - -SuggestionsWithRef.displayName = 'SuggestionsWithRef'; +export default forwardRef(Suggestions); -export default SuggestionsWithRef; +export type {SuggestionProps}; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index b26dc167ed44..a4d29967be1e 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -20,7 +20,7 @@ type Icon = { type: AvatarType; /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name: string; + name?: string; /** Avatar id */ id?: number | string; From 717a5e8d5f3229cf1611c14382adfb0ae5cd1268 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Sun, 21 Jan 2024 14:38:40 +0100 Subject: [PATCH 044/297] fix: started migrating SuggestionEmoji component --- .../ReportActionCompose/SuggestionEmoji.tsx | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index 06089b748554..c5eb13176d44 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -1,8 +1,7 @@ -import PropTypes from 'prop-types'; -import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import EmojiSuggestions from '@components/EmojiSuggestions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; @@ -10,22 +9,12 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -// eslint-disable-next-line import/no-cycle -import {SuggestionProps} from './Suggestions'; +import type {SuggestionProps} from './Suggestions'; -/** - * Check if this piece of string looks like an emoji - */ -const isEmojiCode = (str: string, pos: number): boolean => { - const leftWords = str.slice(0, pos).split(CONST.REGEX.SPECIAL_CHAR_OR_EMOJI); - const leftWord = leftWords.at(-1) ?? ''; - return CONST.REGEX.HAS_COLON_ONLY_AT_THE_BEGINNING.test(leftWord) && leftWord.length > 2; -}; - -const defaultSuggestionsValues = { - suggestedEmojis: [], - colonSignIndex: -1, - shouldShowSuggestionMenu: false, +type SuggestionsValue = { + suggestedEmojis: any[]; + colonIndex: number; + shouldShowSuggestionMenu: boolean; }; type SuggestionEmojiOnyxProps = { @@ -39,6 +28,21 @@ type SuggestionEmojiProps = { } & SuggestionEmojiOnyxProps & SuggestionProps; +/** + * Check if this piece of string looks like an emoji + */ +const isEmojiCode = (str: string, pos: number): boolean => { + const leftWords = str.slice(0, pos).split(CONST.REGEX.SPECIAL_CHAR_OR_EMOJI); + const leftWord = leftWords.at(-1) ?? ''; + return CONST.REGEX.HAS_COLON_ONLY_AT_THE_BEGINNING.test(leftWord) && leftWord.length > 2; +}; + +const defaultSuggestionsValues: SuggestionsValue = { + suggestedEmojis: [], + colonIndex: -1, + shouldShowSuggestionMenu: false, +}; + function SuggestionEmoji( { preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, @@ -61,7 +65,7 @@ function SuggestionEmoji( const [highlightedEmojiIndex, setHighlightedEmojiIndex] = useArrowKeyFocusManager({ isActive: isEmojiSuggestionsMenuVisible, - maxIndex: SuggestionsUtils.getMaxArrowIndex(suggestionValues.suggestedEmojis.length, isAutoSuggestionPickerLarge), + maxIndex: SuggestionsUtils.getMaxArrowIndex(suggestionValues.suggestedEmojis.length, !!isAutoSuggestionPickerLarge), shouldExcludeTextAreaNodes: false, }); @@ -75,7 +79,7 @@ function SuggestionEmoji( * @param {Number} selectedEmoji */ const insertSelectedEmoji = useCallback( - (highlightedEmojiIndexInner) => { + (highlightedEmojiIndexInner: number) => { const commentBeforeColon = value.slice(0, suggestionValues.colonIndex); const emojiObject = suggestionValues.suggestedEmojis[highlightedEmojiIndexInner]; const emojiCode = emojiObject.types && emojiObject.types[preferredSkinTone] ? emojiObject.types[preferredSkinTone] : emojiObject.code; From 1d21ded647fbd25ab4d1375133fe345a31beed02 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 11:01:20 +0700 Subject: [PATCH 045/297] fix: Delay in updating green dot and total amount --- src/libs/actions/IOU.js | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ee752a1f0ef..aeff964cac6b 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -329,7 +329,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) { * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) * @param {Array} policyTags * @param {Array} policyCategories - * @param {Boolean} hasOutstandingChildRequest + * @param {Array} needsToBeManuallySubmitted * @returns {Array} - An array containing the optimistic data, success data, and failure data. */ function buildOnyxDataForMoneyRequest( @@ -348,7 +348,7 @@ function buildOnyxDataForMoneyRequest( policy, policyTags, policyCategories, - hasOutstandingChildRequest = false, + needsToBeManuallySubmitted, ) { const isScanRequest = TransactionUtils.isScanRequest(transaction); const optimisticData = [ @@ -361,7 +361,7 @@ function buildOnyxDataForMoneyRequest( lastReadTime: DateUtils.getDBTime(), lastMessageTranslationKey: '', iouReportID: iouReport.reportID, - hasOutstandingChildRequest, + hasOutstandingChildRequest: needsToBeManuallySubmitted && iouReport.managerID === userAccountID && iouReport.total !== 0, ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, }, @@ -687,7 +687,7 @@ function getMoneyRequestInformation( let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; // Check if the Scheduled Submit is enabled in case of expense report - let needsToBeManuallySubmitted = false; + let needsToBeManuallySubmitted = true; let isFromPaidPolicy = false; if (isPolicyExpenseChat) { isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); @@ -806,10 +806,6 @@ function getMoneyRequestInformation( } : undefined; - // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off - // so the employee has to submit to their manager manually. - const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; - // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( chatReport, @@ -827,7 +823,7 @@ function getMoneyRequestInformation( policy, policyTags, policyCategories, - hasOutstandingChildRequest, + needsToBeManuallySubmitted, ); return { @@ -2544,7 +2540,19 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView let updatedIOUReport = {...iouReport}; const updatedReportPreviewAction = {...reportPreviewAction}; updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; - if (ReportUtils.isExpenseReport(iouReport)) { + + const isPolicyExpenseChat = ReportUtils.isExpenseReport(iouReport); + + let needsToBeManuallySubmitted = true; + if (isPolicyExpenseChat) { + const policy = ReportUtils.getPolicy(iouReport.policyID); + const isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); + + // If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN + needsToBeManuallySubmitted = isFromPaidPolicy && !(policy.isHarvestingEnabled || false); + } + + if (isPolicyExpenseChat) { updatedIOUReport = {...iouReport}; // Because of the Expense reports are stored as negative values, we add the total from the amount @@ -2646,6 +2654,13 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView }, ] : []), + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + hasOutstandingChildRequest: needsToBeManuallySubmitted && updatedIOUReport.managerID === userAccountID && updatedIOUReport.total !== 0, + }, + }, ]; const successData = [ From 7c7132531962d803f64392171d7c3c9d95fc9b6c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 13:55:47 +0100 Subject: [PATCH 046/297] fix: migrate SuggestionEmoji --- src/libs/EmojiUtils.ts | 2 +- .../ReportActionCompose.tsx | 2 +- .../ReportActionCompose/SuggestionEmoji.tsx | 51 ++++++++----------- .../ReportActionCompose/Suggestions.tsx | 2 +- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index e34fa0b90fc6..7971e6147c19 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -384,7 +384,7 @@ function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST * Suggest emojis when typing emojis prefix after colon * @param [limit] - matching emojis limit */ -function suggestEmojis(text: string, lang: keyof SupportedLanguage, limit = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { +function suggestEmojis(text: string, lang: SupportedLanguage, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 025b7142df0d..9754d45be834 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -56,7 +56,7 @@ type SuggestionsRef = { triggerHotkeyActions: (event: KeyboardEvent) => void; updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; - getSuggestions: () => Mention[]; + getSuggestions: () => Mention[] | Emoji[]; }; type ReportActionComposeOnyxProps = { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index c5eb13176d44..dac59065371d 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -1,25 +1,29 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {Emoji} from '@assets/emojis/types'; import EmojiSuggestions from '@components/EmojiSuggestions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; +import type {SupportedLanguage} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {SuggestionsRef} from './ReportActionCompose'; import type {SuggestionProps} from './Suggestions'; type SuggestionsValue = { - suggestedEmojis: any[]; + suggestedEmojis: Emoji[]; colonIndex: number; shouldShowSuggestionMenu: boolean; }; type SuggestionEmojiOnyxProps = { /** Preferred skin tone */ - preferredSkinTone: OnyxEntry; + preferredSkinTone: number; }; type SuggestionEmojiProps = { @@ -47,21 +51,19 @@ function SuggestionEmoji( { preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, value, - setValue, selection, setSelection, updateComment, - isComposerFullSize, isAutoSuggestionPickerLarge, resetKeyboardInput, measureParentContainer, isComposerFocused, }: SuggestionEmojiProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); - const isEmojiSuggestionsMenuVisible = !_.isEmpty(suggestionValues.suggestedEmojis) && suggestionValues.shouldShowSuggestionMenu; + const isEmojiSuggestionsMenuVisible = suggestionValues.suggestedEmojis.length > 0 && suggestionValues.shouldShowSuggestionMenu; const [highlightedEmojiIndex, setHighlightedEmojiIndex] = useArrowKeyFocusManager({ isActive: isEmojiSuggestionsMenuVisible, @@ -82,7 +84,7 @@ function SuggestionEmoji( (highlightedEmojiIndexInner: number) => { const commentBeforeColon = value.slice(0, suggestionValues.colonIndex); const emojiObject = suggestionValues.suggestedEmojis[highlightedEmojiIndexInner]; - const emojiCode = emojiObject.types && emojiObject.types[preferredSkinTone] ? emojiObject.types[preferredSkinTone] : emojiObject.code; + const emojiCode = emojiObject.types?.[preferredSkinTone] ? emojiObject.types[preferredSkinTone] : emojiObject.code; const commentAfterColonWithEmojiNameRemoved = value.slice(selection.end); updateComment(`${commentBeforeColon}${emojiCode} ${SuggestionsUtils.trimLeadingSpace(commentAfterColonWithEmojiNameRemoved)}`, true); @@ -119,11 +121,9 @@ function SuggestionEmoji( /** * Listens for keyboard shortcuts and applies the action - * - * @param {Object} e */ const triggerHotkeyActions = useCallback( - (e) => { + (e: KeyboardEvent) => { const suggestionsExist = suggestionValues.suggestedEmojis.length > 0; if (((!e.shiftKey && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) || e.key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) && suggestionsExist) { @@ -151,7 +151,7 @@ function SuggestionEmoji( * Calculates and cares about the content of an Emoji Suggester */ const calculateEmojiSuggestion = useCallback( - (selectionEnd) => { + (selectionEnd: number) => { if (shouldBlockCalc.current || !value) { shouldBlockCalc.current = false; resetSuggestions(); @@ -161,16 +161,16 @@ function SuggestionEmoji( const colonIndex = leftString.lastIndexOf(':'); const isCurrentlyShowingEmojiSuggestion = isEmojiCode(value, selectionEnd); - const nextState = { + const nextState: SuggestionsValue = { suggestedEmojis: [], colonIndex, shouldShowSuggestionMenu: false, }; - const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale); + const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale as SupportedLanguage); - if (newSuggestedEmojis.length && isCurrentlyShowingEmojiSuggestion) { + if (newSuggestedEmojis?.length && isCurrentlyShowingEmojiSuggestion) { nextState.suggestedEmojis = newSuggestedEmojis; - nextState.shouldShowSuggestionMenu = !_.isEmpty(newSuggestedEmojis); + nextState.shouldShowSuggestionMenu = !isEmptyObject(newSuggestedEmojis); } setSuggestionValues((prevState) => ({...prevState, ...nextState})); @@ -187,7 +187,7 @@ function SuggestionEmoji( }, [selection, calculateEmojiSuggestion, isComposerFocused]); const onSelectionChange = useCallback( - (e) => { + (e: NativeSyntheticEvent) => { /** * we pass here e.nativeEvent.selection.end directly to calculateEmojiSuggestion * because in other case calculateEmojiSuggestion will have an old calculation value @@ -199,7 +199,7 @@ function SuggestionEmoji( ); const setShouldBlockSuggestionCalc = useCallback( - (shouldBlockSuggestionCalc) => { + (shouldBlockSuggestionCalc: boolean) => { shouldBlockCalc.current = shouldBlockSuggestionCalc; }, [shouldBlockCalc], @@ -207,10 +207,6 @@ function SuggestionEmoji( const getSuggestions = useCallback(() => suggestionValues.suggestedEmojis, [suggestionValues]); - const resetEmojiSuggestions = useCallback(() => { - setSuggestionValues((prevState) => ({...prevState, suggestedEmojis: []})); - }, []); - useImperativeHandle( ref, () => ({ @@ -230,17 +226,12 @@ function SuggestionEmoji( return ( ); @@ -248,9 +239,11 @@ function SuggestionEmoji( SuggestionEmoji.displayName = 'SuggestionEmoji'; +const SuggestionEmojiForwardedRef = forwardRef(SuggestionEmoji); + export default withOnyx({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, selector: EmojiUtils.getPreferredSkinToneIndex, }, -})(forwardRef(SuggestionEmoji)); +})(SuggestionEmojiForwardedRef); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index f997637a8c4c..3a009728a915 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -89,7 +89,7 @@ function Suggestions( }, []); const onSelectionChange = useCallback((e: NativeSyntheticEvent) => { - const emojiHandler = suggestionEmojiRef.current?.onSelectionChange(e); + const emojiHandler = suggestionEmojiRef.current?.onSelectionChange?.(e); return emojiHandler; }, []); From be57be30f750da1ca8a1374213907c3c8e8b8eb1 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 14:11:08 +0100 Subject: [PATCH 047/297] fix: SuggestionEmoji and Suggestions types, migrate SendButton --- .../{SendButton.js => SendButton.tsx} | 14 +++++++------- .../report/ReportActionCompose/SuggestionEmoji.tsx | 12 +++++------- .../report/ReportActionCompose/Suggestions.tsx | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) rename src/pages/home/report/ReportActionCompose/{SendButton.js => SendButton.tsx} (90%) diff --git a/src/pages/home/report/ReportActionCompose/SendButton.js b/src/pages/home/report/ReportActionCompose/SendButton.tsx similarity index 90% rename from src/pages/home/report/ReportActionCompose/SendButton.js rename to src/pages/home/report/ReportActionCompose/SendButton.tsx index d0b0453ace2f..b6c6200fc7c0 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.js +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; @@ -11,21 +10,21 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -const propTypes = { +type SendButtonProps = { /** Whether the button is disabled */ - isDisabled: PropTypes.bool.isRequired, + isDisabled: boolean; /** Handle clicking on send button */ - handleSendMessage: PropTypes.func.isRequired, + handleSendMessage: () => void; }; -function SendButton({isDisabled: isDisabledProp, handleSendMessage}) { +function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); const Tap = Gesture.Tap() - .enabled() + .enabled(!isDisabledProp) .onEnd(() => { handleSendMessage(); }); @@ -46,6 +45,8 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}) { ]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.send')} + accessible + onPress={() => {}} > {({pressed}) => ( void; + resetKeyboardInput: (() => void) | undefined; } & SuggestionEmojiOnyxProps & SuggestionProps; @@ -92,7 +92,7 @@ function SuggestionEmoji( // In some Android phones keyboard, the text to search for the emoji is not cleared // will be added after the user starts typing again on the keyboard. This package is // a workaround to reset the keyboard natively. - resetKeyboardInput(); + resetKeyboardInput?.(); setSelection({ start: suggestionValues.colonIndex + emojiCode.length + CONST.SPACE_LENGTH, @@ -239,11 +239,9 @@ function SuggestionEmoji( SuggestionEmoji.displayName = 'SuggestionEmoji'; -const SuggestionEmojiForwardedRef = forwardRef(SuggestionEmoji); - -export default withOnyx({ +export default withOnyx, SuggestionEmojiOnyxProps>({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, selector: EmojiUtils.getPreferredSkinToneIndex, }, -})(SuggestionEmojiForwardedRef); +})(forwardRef(SuggestionEmoji)); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 3a009728a915..2500af40c7c6 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -22,7 +22,7 @@ type SuggestionProps = { measureParentContainer: () => void; isComposerFullSize: boolean; isComposerFocused?: boolean; - resetKeyboardInput: () => void; + resetKeyboardInput?: () => void; isAutoSuggestionPickerLarge?: boolean; composerHeight?: number; }; From 929a51a9a8be77696c674b881b7835d85147e481 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 14:38:55 +0100 Subject: [PATCH 048/297] fix: migrate AttachmentPickerWithMenuItems --- src/components/PopoverMenu.tsx | 2 +- ...s.js => AttachmentPickerWithMenuItems.tsx} | 119 ++++++++---------- 2 files changed, 52 insertions(+), 69 deletions(-) rename src/pages/home/report/ReportActionCompose/{AttachmentPickerWithMenuItems.js => AttachmentPickerWithMenuItems.tsx} (82%) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 17b1a119671a..b411de1103f1 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -69,7 +69,7 @@ type PopoverMenuProps = Partial & { anchorPosition: AnchorPosition; /** Ref of the anchor */ - anchorRef: RefObject; + anchorRef: RefObject; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx similarity index 82% rename from src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js rename to src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 444dd939142b..547283cc4eb0 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -1,23 +1,23 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import {useIsFocused} from '@react-navigation/native'; +import type {FC} from 'react'; import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {SvgProps} from 'react-native-svg'; +import type {ValueOf} from 'type-fest'; import AttachmentPicker from '@components/AttachmentPicker'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; -import withNavigationFocus from '@components/withNavigationFocus'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; @@ -26,88 +26,77 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; -const propTypes = { - /** The report currently being looked at */ - report: PropTypes.shape({ - /** ID of the report */ - reportID: PropTypes.string, +type MoneyRequestOption = { + icon: FC; + text: string; + onSelected: () => void; +}; - /** Whether or not the report is in the process of being created */ - loading: PropTypes.bool, - }).isRequired, +type MoneyRequestOptions = Record, MoneyRequestOption>; +type AttachmentPickerWithMenuItemsOnyxProps = { /** The policy tied to the report */ - policy: PropTypes.shape({ - /** Type of the policy */ - type: PropTypes.string, - }), + policy: OnyxEntry; +}; - /** The personal details of everyone in the report */ - reportParticipantIDs: PropTypes.arrayOf(PropTypes.number), +type AttachmentPickerWithMenuItemsProps = { + /** The report currently being looked at */ + report: OnyxTypes.Report; /** Callback to open the file in the modal */ - displayFileInModal: PropTypes.func.isRequired, + displayFileInModal: (url: string) => void; /** Whether or not the full size composer is available */ - isFullComposerAvailable: PropTypes.bool.isRequired, + isFullComposerAvailable: boolean; /** Whether or not the composer is full size */ - isComposerFullSize: PropTypes.bool.isRequired, + isComposerFullSize: boolean; /** Whether or not the user is blocked from concierge */ - isBlockedFromConcierge: PropTypes.bool.isRequired, + isBlockedFromConcierge: boolean; /** Whether or not the attachment picker is disabled */ - disabled: PropTypes.bool.isRequired, + disabled: boolean; /** Sets the menu visibility */ - setMenuVisibility: PropTypes.func.isRequired, + setMenuVisibility: (isVisible: boolean) => void; /** Whether or not the menu is visible */ - isMenuVisible: PropTypes.bool.isRequired, + isMenuVisible: boolean; /** Report ID */ - reportID: PropTypes.string.isRequired, + reportID: string; /** Called when opening the attachment picker */ - onTriggerAttachmentPicker: PropTypes.func.isRequired, + onTriggerAttachmentPicker: () => void; /** Called when cancelling the attachment picker */ - onCanceledAttachmentPicker: PropTypes.func.isRequired, + onCanceledAttachmentPicker: () => void; /** Called when the menu with the items is closed after it was open */ - onMenuClosed: PropTypes.func.isRequired, + onMenuClosed: () => void; /** Called when the add action button is pressed */ - onAddActionPressed: PropTypes.func.isRequired, + onAddActionPressed: () => void; /** Called when the menu item is selected */ - onItemSelected: PropTypes.func.isRequired, + onItemSelected: () => void; /** A ref for the add action button */ - actionButtonRef: PropTypes.shape({ - // eslint-disable-next-line react/forbid-prop-types - current: PropTypes.object, - }).isRequired, - - /** Whether or not the screen is focused */ - isFocused: PropTypes.bool.isRequired, + actionButtonRef: React.RefObject; /** A function that toggles isScrollLikelyLayoutTriggered flag for a certain period of time */ - raiseIsScrollLikelyLayoutTriggered: PropTypes.func.isRequired, -}; + raiseIsScrollLikelyLayoutTriggered: () => void; -const defaultProps = { - reportParticipantIDs: [], - policy: {}, -}; + /** The personal details of everyone in the report */ + reportParticipantIDs?: number[]; +} & AttachmentPickerWithMenuItemsOnyxProps; /** * This includes the popover of options you see when pressing the + button in the composer. * It also contains the attachment picker, as the menu items need to be able to open it. - * - * @returns {React.Component} */ function AttachmentPickerWithMenuItems({ report, @@ -127,9 +116,9 @@ function AttachmentPickerWithMenuItems({ onAddActionPressed, onItemSelected, actionButtonRef, - isFocused, raiseIsScrollLikelyLayoutTriggered, -}) { +}: AttachmentPickerWithMenuItemsProps) { + const isFocused = useIsFocused(); const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -137,10 +126,9 @@ function AttachmentPickerWithMenuItems({ /** * Returns the list of IOU Options - * @returns {Array} */ const moneyRequestOptions = useMemo(() => { - const options = { + const options: MoneyRequestOptions = { [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Receipt, text: translate('iou.splitBill'), @@ -158,16 +146,15 @@ function AttachmentPickerWithMenuItems({ }, }; - return _.map(ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs), (option) => ({ + return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({ ...options[option], })); }, [report, policy, reportParticipantIDs, translate]); /** * Determines if we can show the task option - * @returns {Boolean} */ - const taskOption = useMemo(() => { + const taskOption: MoneyRequestOption[] = useMemo(() => { if (!ReportUtils.canCreateTaskInReport(report)) { return []; } @@ -206,6 +193,7 @@ function AttachmentPickerWithMenuItems({ return ( + {/* @ts-expect-error TODO: Remove this once SettlementButton (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} {({openPicker}) => { const triggerAttachmentPicker = () => { onTriggerAttachmentPicker(); @@ -235,7 +223,7 @@ function AttachmentPickerWithMenuItems({ { - e.preventDefault(); + e?.preventDefault(); raiseIsScrollLikelyLayoutTriggered(); Report.setIsComposerFullSize(reportID, false); }} @@ -257,7 +245,7 @@ function AttachmentPickerWithMenuItems({ { - e.preventDefault(); + e?.preventDefault(); raiseIsScrollLikelyLayoutTriggered(); Report.setIsComposerFullSize(reportID, true); }} @@ -279,14 +267,14 @@ function AttachmentPickerWithMenuItems({ { - e.preventDefault(); + e?.preventDefault(); if (!isFocused) { return; } onAddActionPressed(); // Drop focus to avoid blue focus ring. - actionButtonRef.current.blur(); + actionButtonRef.current?.blur(); setMenuVisibility(!isMenuVisible); }} style={styles.composerSizeButton} @@ -329,15 +317,10 @@ function AttachmentPickerWithMenuItems({ ); } -AttachmentPickerWithMenuItems.propTypes = propTypes; -AttachmentPickerWithMenuItems.defaultProps = defaultProps; AttachmentPickerWithMenuItems.displayName = 'AttachmentPickerWithMenuItems'; -export default compose( - withNavigationFocus, - withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${lodashGet(report, 'policyID')}`, - }, - }), -)(AttachmentPickerWithMenuItems); +export default withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, + }, +})(AttachmentPickerWithMenuItems); From 0cc6502443d17483711b9a26bd010e878e944626 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 15:09:18 +0100 Subject: [PATCH 049/297] fix: Migrate SilentCommentUpdater --- ...estions.js => ComposerWithSuggestions.tsx} | 0 .../{index.android.js => index.android.tsx} | 22 ++-------- .../{index.js => index.tsx} | 42 +++---------------- .../SilentCommentUpdater/types.ts | 29 +++++++++++++ 4 files changed, 38 insertions(+), 55 deletions(-) rename src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/{ComposerWithSuggestions.js => ComposerWithSuggestions.tsx} (100%) rename src/pages/home/report/ReportActionCompose/SilentCommentUpdater/{index.android.js => index.android.tsx} (69%) rename src/pages/home/report/ReportActionCompose/SilentCommentUpdater/{index.js => index.tsx} (60%) create mode 100644 src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx similarity index 100% rename from src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js rename to src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx similarity index 69% rename from src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.js rename to src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx index f924a7b59194..cee9cbf794bf 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx @@ -1,19 +1,7 @@ -import PropTypes from 'prop-types'; import {useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** The comment of the report */ - comment: PropTypes.string, - - /** Updates the comment */ - updateComment: PropTypes.func.isRequired, -}; - -const defaultProps = { - comment: '', -}; +import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './types'; /** * Adding .android component to disable updating comment when prev comment is different @@ -24,9 +12,8 @@ const defaultProps = { * This component doesn't render anything. It runs a side effect to update the comment of a report under certain conditions. * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. - * @returns {null} */ -function SilentCommentUpdater({comment, updateComment}) { +function SilentCommentUpdater({comment = '', updateComment}: SilentCommentUpdaterProps) { useEffect(() => { updateComment(comment); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount @@ -35,13 +22,10 @@ function SilentCommentUpdater({comment, updateComment}) { return null; } -SilentCommentUpdater.propTypes = propTypes; -SilentCommentUpdater.defaultProps = defaultProps; SilentCommentUpdater.displayName = 'SilentCommentUpdater'; -export default withOnyx({ +export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, - initialValue: '', }, })(SilentCommentUpdater); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx similarity index 60% rename from src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js rename to src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index 9aa997a892f4..afee06dbc8ff 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -1,46 +1,18 @@ -import PropTypes from 'prop-types'; import {useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** The comment of the report */ - comment: PropTypes.string, - - /** The report associated with the comment */ - report: PropTypes.shape({ - /** The ID of the report */ - reportID: PropTypes.string, - }).isRequired, - - /** The value of the comment */ - value: PropTypes.string.isRequired, - - /** The ref of the comment */ - commentRef: PropTypes.shape({ - /** The current value of the comment */ - current: PropTypes.string, - }).isRequired, - - /** Updates the comment */ - updateComment: PropTypes.func.isRequired, -}; - -const defaultProps = { - comment: '', -}; +import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './types'; /** * This component doesn't render anything. It runs a side effect to update the comment of a report under certain conditions. * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. - * @returns {null} */ -function SilentCommentUpdater({comment, commentRef, report, value, updateComment}) { +function SilentCommentUpdater({comment = '', commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { const prevCommentProp = usePrevious(comment); - const prevReportId = usePrevious(report.reportID); + const prevReportId = usePrevious(report?.reportID); const {preferredLocale} = useLocalize(); const prevPreferredLocale = usePrevious(preferredLocale); @@ -56,21 +28,19 @@ function SilentCommentUpdater({comment, commentRef, report, value, updateComment // As the report IDs change, make sure to update the composer comment as we need to make sure // we do not show incorrect data in there (ie. draft of message from other report). - if (preferredLocale === prevPreferredLocale && report.reportID === prevReportId && !shouldSyncComment) { + if (preferredLocale === prevPreferredLocale && report?.reportID === prevReportId && !shouldSyncComment) { return; } updateComment(comment); - }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report.reportID, updateComment, value, commentRef]); + }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report?.reportID, updateComment, value, commentRef]); return null; } -SilentCommentUpdater.propTypes = propTypes; -SilentCommentUpdater.defaultProps = defaultProps; SilentCommentUpdater.displayName = 'SilentCommentUpdater'; -export default withOnyx({ +export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, initialValue: '', diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts new file mode 100644 index 000000000000..60924f24099b --- /dev/null +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -0,0 +1,29 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {Report} from '@src/types/onyx'; + +type SilentCommentUpdaterOnyxProps = { + /** The comment of the report */ + comment: OnyxEntry; +}; + +type SilentCommentUpdaterProps = { + /** Updates the comment */ + updateComment: (comment: OnyxEntry) => void; + + /** The ID of the report associated with the comment */ + reportID: string; + + /** The report associated with the comment */ + report: OnyxEntry; + + /** The value of the comment */ + value: string; + + /** The ref of the comment */ + commentRef: React.RefObject; + + /** The comment of the report */ + commnet: string; +} & SilentCommentUpdaterOnyxProps; + +export type {SilentCommentUpdaterProps, SilentCommentUpdaterOnyxProps}; From 8ed208cb62610d729bd6a7d1c9da1ec477b96c68 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 23 Jan 2024 10:49:06 +0100 Subject: [PATCH 050/297] fix: ComposerWithSuggestion migration --- .../AttachmentPickerWithMenuItems.tsx | 8 +- .../ComposerWithSuggestions.tsx | 239 ++++++++++-------- .../ReportActionCompose.tsx | 9 +- 3 files changed, 146 insertions(+), 110 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 547283cc4eb0..0dbfbba2759d 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -43,7 +43,7 @@ type AttachmentPickerWithMenuItemsOnyxProps = { type AttachmentPickerWithMenuItemsProps = { /** The report currently being looked at */ - report: OnyxTypes.Report; + report: OnyxEntry; /** Callback to open the file in the modal */ displayFileInModal: (url: string) => void; @@ -132,17 +132,17 @@ function AttachmentPickerWithMenuItems({ [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Receipt, text: translate('iou.splitBill'), - onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.SPLIT, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report.reportID)), + onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.SPLIT, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report?.reportID ?? '')), }, [CONST.IOU.TYPE.REQUEST]: { icon: Expensicons.MoneyCircle, text: translate('iou.requestMoney'), - onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.REQUEST, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report.reportID)), + onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.REQUEST, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report?.reportID ?? '')), }, [CONST.IOU.TYPE.SEND]: { icon: Expensicons.Send, text: translate('iou.sendMoney'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report.reportID), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID), }, }; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 413807b1f992..bce7d80121b4 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,11 +1,14 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import lodashDebounce from 'lodash/debounce'; +import type {ForwardedRef, RefAttributes, RefObject} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {MeasureInWindowOnSuccessCallback} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; -import withKeyboardState from '@components/withKeyboardState'; +import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -14,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import compose from '@libs/compose'; import * as ComposerUtils from '@libs/ComposerUtils'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; @@ -28,6 +30,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionUtils from '@libs/SuggestionUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; +import type {ComposerRef, SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; @@ -36,17 +39,65 @@ import * as Report from '@userActions/Report'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultProps, propTypes} from './composerWithSuggestionsProps'; +type ComposerWithSuggestionsOnyxProps = { + /** The number of lines the comment should take up */ + numberOfLines: number; + + /** The parent report actions for the report */ + parentReportActions: OnyxEntry; + + /** The modal state */ + modal: OnyxEntry; + + /** The preferred skin tone of the user */ + preferredSkinTone: number; + + /** Whether the input is focused */ + editFocused: boolean; +}; +type ComposerWithSuggestionsProps = { + reportID: string; + report: OnyxEntry; + reportActions: OnyxTypes.ReportAction[] | undefined; + onFocus: () => void; + onBlur: (event: FocusEvent) => void; + onValueChange: (value: string) => void; + isComposerFullSize: boolean; + isMenuVisible: boolean; + inputPlaceholder: string; + displayFileInModal: (fileURL: string) => void; + textInputShouldClear: boolean; + setTextInputShouldClear: (shouldClear: boolean) => void; + isBlockedFromConcierge: boolean; + disabled: boolean; + isFullComposerAvailable: boolean; + setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + setIsCommentEmpty: (isCommentEmpty: boolean) => void; + handleSendMessage: () => void; + shouldShowComposeInput: OnyxEntry; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + listHeight: number; + isScrollLikelyLayoutTriggered: RefObject; + raiseIsScrollLikelyLayoutTriggered: () => void; + suggestionsRef: React.RefObject; + animatedRef: RefObject; + isNextModalWillOpenRef: RefObject; + editFocused: boolean; +} & ComposerWithSuggestionsOnyxProps & + Partial; + const {RNTextInputReset} = NativeModules; const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; /** * Broadcast that the user is typing. Debounced to limit how often we publish client events. - * @param {String} reportID */ -const debouncedBroadcastUserIsTyping = _.debounce((reportID) => { +const debouncedBroadcastUserIsTyping = lodashDebounce((reportID: string) => { Report.broadcastUserIsTyping(reportID); }, 100); @@ -61,61 +112,61 @@ const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); * If a component really needs access to these state values it should be put here. * However, double check if the component really needs access, as it will re-render * on every key press. - * @param {Object} props - * @returns {React.Component} */ -function ComposerWithSuggestions({ - // Onyx - modal, - preferredSkinTone, - parentReportActions, - numberOfLines, - // HOCs - isKeyboardShown, - // Props: Report - reportID, - report, - reportActions, - // Focus - onFocus, - onBlur, - onValueChange, - // Composer - isComposerFullSize, - isMenuVisible, - inputPlaceholder, - displayFileInModal, - textInputShouldClear, - setTextInputShouldClear, - isBlockedFromConcierge, - disabled, - isFullComposerAvailable, - setIsFullComposerAvailable, - setIsCommentEmpty, - handleSendMessage, - shouldShowComposeInput, - measureParentContainer, - listHeight, - isScrollLikelyLayoutTriggered, - raiseIsScrollLikelyLayoutTriggered, - // Refs - suggestionsRef, - animatedRef, - forwardedRef, - isNextModalWillOpenRef, - editFocused, - // For testing - children, -}) { +function ComposerWithSuggestions( + { + // Onyx + modal, + preferredSkinTone, + parentReportActions, + numberOfLines, + + // Props: Report + reportID, + report, + reportActions, + // Focus + onFocus, + onBlur, + onValueChange, + // Composer + isComposerFullSize, + isMenuVisible, + inputPlaceholder, + displayFileInModal, + textInputShouldClear, + setTextInputShouldClear, + isBlockedFromConcierge, + disabled, + isFullComposerAvailable, + setIsFullComposerAvailable, + setIsCommentEmpty, + handleSendMessage, + shouldShowComposeInput, + measureParentContainer, + listHeight, + isScrollLikelyLayoutTriggered, + raiseIsScrollLikelyLayoutTriggered, + // Refs + suggestionsRef, + animatedRef, + isNextModalWillOpenRef, + editFocused, + // For testing + children, + }: ComposerWithSuggestionsProps, + ref: ForwardedRef, +) { + const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {preferredLocale} = useLocalize(); const isFocused = useIsFocused(); const navigation = useNavigation(); - const emojisPresentBefore = useRef([]); + const emojisPresentBefore = useRef([]); const [value, setValue] = useState(() => { - const draft = getDraftComment(reportID) || ''; + const draft = getDraftComment(reportID) ?? ''; if (draft) { emojisPresentBefore.current = EmojiUtils.extractEmojis(draft); } @@ -126,9 +177,9 @@ function ComposerWithSuggestions({ const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput; + const isEmptyChat = useMemo(() => reportActions?.length === 1, [reportActions]); + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; + const shouldAutoFocus = !modal?.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput; const valueRef = useRef(value); valueRef.current = value; @@ -138,11 +189,11 @@ function ComposerWithSuggestions({ const [composerHeight, setComposerHeight] = useState(0); const textInputRef = useRef(null); - const insertedEmojisRef = useRef([]); + const insertedEmojisRef = useRef([]); const syncSelectionWithOnChangeTextRef = useRef(null); - const suggestions = lodashGet(suggestionsRef, 'current.getSuggestions', () => [])(); + const suggestions = suggestionsRef.current?.getSuggestions(); const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions.length); @@ -159,15 +210,12 @@ function ComposerWithSuggestions({ /** * Set the TextInput Ref - * - * @param {Element} el - * @memberof ReportActionCompose */ const setTextInputRef = useCallback( (el) => { ReportActionComposeFocusManager.composerRef.current = el; textInputRef.current = el; - if (_.isFunction(animatedRef)) { + if (typeof animatedRef === 'function') { animatedRef(el); } }, @@ -196,7 +244,7 @@ function ComposerWithSuggestions({ * @param {Boolean} shouldDebounceSaveComment */ const updateComment = useCallback( - (commentValue, shouldDebounceSaveComment) => { + (commentValue: string, shouldDebounceSaveComment: boolean) => { raiseIsScrollLikelyLayoutTriggered(); const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); if (!_.isEmpty(emojis)) { @@ -514,7 +562,7 @@ function ComposerWithSuggestions({ }, []); useImperativeHandle( - forwardedRef, + ref, () => ({ blur, focus, @@ -608,39 +656,28 @@ ComposerWithSuggestions.propTypes = propTypes; ComposerWithSuggestions.defaultProps = defaultProps; ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; -const ComposerWithSuggestionsWithRef = React.forwardRef((props, ref) => ( - -)); - -ComposerWithSuggestionsWithRef.displayName = 'ComposerWithSuggestionsWithRef'; - -export default compose( - withKeyboardState, - withOnyx({ - numberOfLines: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, - // We might not have number of lines in onyx yet, for which the composer would be rendered as null - // during the first render, which we want to avoid: - initWithStoredValues: false, - }, - modal: { - key: ONYXKEYS.MODAL, - }, - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - selector: EmojiUtils.getPreferredSkinToneIndex, - }, - editFocused: { - key: ONYXKEYS.INPUT_FOCUSED, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - canEvict: false, - initWithStoredValues: false, - }, - }), -)(ComposerWithSuggestionsWithRef); +const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); + +export default withOnyx, ComposerWithSuggestionsOnyxProps>({ + numberOfLines: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, + // We might not have number of lines in onyx yet, for which the composer would be rendered as null + // during the first render, which we want to avoid: + initWithStoredValues: false, + }, + modal: { + key: ONYXKEYS.MODAL, + }, + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + selector: EmojiUtils.getPreferredSkinToneIndex, + }, + editFocused: { + key: ONYXKEYS.INPUT_FOCUSED, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + canEvict: false, + initWithStoredValues: false, + }, +})(ComposerWithSuggestionsWithRef); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 9754d45be834..b46eb145173e 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -6,6 +6,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; +import type {Emoji} from '@assets/emojis/types'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; @@ -389,7 +390,6 @@ function ReportActionCompose({ {({displayFileInModal}) => ( <> Date: Wed, 24 Jan 2024 09:45:39 +0100 Subject: [PATCH 051/297] fix: keep working on ComposerWithSuggestions --- src/components/Composer/index.tsx | 5 +- src/libs/updateMultilineInputRange/types.ts | 2 +- .../ComposerWithSuggestions.tsx | 110 +++++++++--------- .../{index.e2e.js => index.e2e.tsx} | 0 .../{index.js => index.tsx} | 0 .../ReportActionCompose.tsx | 2 +- .../SilentCommentUpdater/index.tsx | 2 +- .../ReportActionCompose/Suggestions.tsx | 4 +- 8 files changed, 65 insertions(+), 60 deletions(-) rename src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/{index.e2e.js => index.e2e.tsx} (100%) rename src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/{index.js => index.tsx} (100%) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3c2caf020ef7..e78e0c2a4039 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -4,9 +4,8 @@ import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports -import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; +import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; @@ -75,7 +74,7 @@ function Composer( shouldContainScroll = false, ...props }: ComposerProps, - ref: ForwardedRef>>, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/libs/updateMultilineInputRange/types.ts b/src/libs/updateMultilineInputRange/types.ts index d1b134b09a99..ce8f553c51f8 100644 --- a/src/libs/updateMultilineInputRange/types.ts +++ b/src/libs/updateMultilineInputRange/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput, shouldAutoFocus?: boolean) => void; +type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput | null, shouldAutoFocus?: boolean) => void; export default UpdateMultilineInputRange; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index bce7d80121b4..cb5f2b52ffbc 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -2,10 +2,11 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefAttributes, RefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {MeasureInWindowOnSuccessCallback} from 'react-native'; +import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {useAnimatedRef} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; import useKeyboardState from '@hooks/useKeyboardState'; @@ -43,9 +44,16 @@ import type * as OnyxTypes from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultProps, propTypes} from './composerWithSuggestionsProps'; +type SyncSelection = { + position: number; + value: string; +}; + +type AnimatedRef = ReturnType; + type ComposerWithSuggestionsOnyxProps = { /** The number of lines the comment should take up */ - numberOfLines: number; + numberOfLines: OnyxEntry; /** The parent report actions for the report */ parentReportActions: OnyxEntry; @@ -57,8 +65,9 @@ type ComposerWithSuggestionsOnyxProps = { preferredSkinTone: number; /** Whether the input is focused */ - editFocused: boolean; + editFocused: OnyxEntry; }; + type ComposerWithSuggestionsProps = { reportID: string; report: OnyxEntry; @@ -69,7 +78,7 @@ type ComposerWithSuggestionsProps = { isComposerFullSize: boolean; isMenuVisible: boolean; inputPlaceholder: string; - displayFileInModal: (fileURL: string) => void; + displayFileInModal: (file: File | undefined) => void; textInputShouldClear: boolean; setTextInputShouldClear: (shouldClear: boolean) => void; isBlockedFromConcierge: boolean; @@ -84,8 +93,8 @@ type ComposerWithSuggestionsProps = { isScrollLikelyLayoutTriggered: RefObject; raiseIsScrollLikelyLayoutTriggered: () => void; suggestionsRef: React.RefObject; - animatedRef: RefObject; - isNextModalWillOpenRef: RefObject; + animatedRef: AnimatedRef; + isNextModalWillOpenRef: RefObject; editFocused: boolean; } & ComposerWithSuggestionsOnyxProps & Partial; @@ -178,7 +187,7 @@ function ComposerWithSuggestions( const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; const isEmptyChat = useMemo(() => reportActions?.length === 1, [reportActions]); - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? ''] ?? null; const shouldAutoFocus = !modal?.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput; const valueRef = useRef(value); @@ -188,14 +197,14 @@ function ComposerWithSuggestions( const [composerHeight, setComposerHeight] = useState(0); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const insertedEmojisRef = useRef([]); - const syncSelectionWithOnChangeTextRef = useRef(null); + const syncSelectionWithOnChangeTextRef = useRef(null); const suggestions = suggestionsRef.current?.getSuggestions(); - const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions.length); + const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions?.length ?? 0); const isAutoSuggestionPickerLarge = !isSmallScreenWidth || (isSmallScreenWidth && hasEnoughSpaceForLargeSuggestion); @@ -212,7 +221,7 @@ function ComposerWithSuggestions( * Set the TextInput Ref */ const setTextInputRef = useCallback( - (el) => { + (el: TextInput) => { ReportActionComposeFocusManager.composerRef.current = el; textInputRef.current = el; if (typeof animatedRef === 'function') { @@ -231,7 +240,7 @@ function ComposerWithSuggestions( const debouncedSaveReportComment = useMemo( () => - _.debounce((selectedReportID, newComment) => { + lodashDebounce((selectedReportID, newComment) => { Report.saveReportComment(selectedReportID, newComment || ''); }, 1000), [], @@ -239,17 +248,14 @@ function ComposerWithSuggestions( /** * Update the value of the comment in Onyx - * - * @param {String} comment - * @param {Boolean} shouldDebounceSaveComment */ const updateComment = useCallback( - (commentValue: string, shouldDebounceSaveComment: boolean) => { + (commentValue: string | null, shouldDebounceSaveComment?: boolean) => { raiseIsScrollLikelyLayoutTriggered(); - const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); - if (!_.isEmpty(emojis)) { + const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentValue ?? '', preferredSkinTone, preferredLocale); + if (emojis.length) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); - if (!_.isEmpty(newEmojis)) { + if (newEmojis.length) { // Ensure emoji suggestions are hidden after inserting emoji even when the selection is not changed if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); @@ -269,7 +275,7 @@ function ComposerWithSuggestions( emojisPresentBefore.current = emojis; setValue(newCommentConverted); if (commentValue !== newComment) { - const position = Math.max(selection.end + (newComment.length - commentRef.current.length), cursorPosition || 0); + const position = Math.max(selection.end + (newComment.length - commentRef.current.length), cursorPosition ?? 0); if (isIOSNative) { syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; @@ -316,10 +322,9 @@ function ComposerWithSuggestions( /** * Update the number of lines for a comment in Onyx - * @param {Number} numberOfLines */ const updateNumberOfLines = useCallback( - (newNumberOfLines) => { + (newNumberOfLines: number) => { if (newNumberOfLines === numberOfLines) { return; } @@ -328,9 +333,6 @@ function ComposerWithSuggestions( [reportID, numberOfLines], ); - /** - * @returns {String} - */ const prepareCommentAndResetComposer = useCallback(() => { const trimmedComment = commentRef.current.trim(); const commentLength = ReportUtils.getCommentLength(trimmedComment); @@ -356,11 +358,9 @@ function ComposerWithSuggestions( /** * Callback to add whatever text is chosen into the main input (used f.e as callback for the emoji picker) - * @param {String} text - * @param {Boolean} shouldAddTrailSpace */ const replaceSelectionWithText = useCallback( - (text, shouldAddTrailSpace = true) => { + (text: string, shouldAddTrailSpace = true) => { const updatedText = shouldAddTrailSpace ? `${text} ` : text; const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0; updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText)); @@ -373,12 +373,12 @@ function ComposerWithSuggestions( ); const triggerHotkeyActions = useCallback( - (e) => { + (e: KeyboardEvent) => { if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } - if (suggestionsRef.current.triggerHotkeyActions(e)) { + if (suggestionsRef.current?.triggerHotkeyActions(e)) { return; } @@ -390,14 +390,20 @@ function ComposerWithSuggestions( // Trigger the edit box for last sent message if ArrowUp is pressed and the comment is empty and Chronos is not in the participants const valueLength = valueRef.current.length; - if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !ReportUtils.chatIncludesChronos(report)) { + if ( + e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && + textInputRef.current && + 'selectionStart' in textInputRef.current && + textInputRef.current?.selectionStart === 0 && + valueLength === 0 && + !ReportUtils.chatIncludesChronos(report) + ) { e.preventDefault(); - const lastReportAction = _.find( - [...reportActions, parentReportAction], + const lastReportAction = [...(reportActions ?? []), parentReportAction].find( (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action), ); if (lastReportAction) { - Report.saveReportActionDraft(reportID, lastReportAction, _.last(lastReportAction.message).html); + Report.saveReportActionDraft(reportID, lastReportAction, lastReportAction.message?.at(-1)?.html ?? ''); } } }, @@ -405,7 +411,7 @@ function ComposerWithSuggestions( ); const onChangeText = useCallback( - (commentValue) => { + (commentValue: string) => { updateComment(commentValue, true); if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { @@ -416,7 +422,7 @@ function ComposerWithSuggestions( InteractionManager.runAfterInteractions(() => { // note: this implementation is only available on non-web RN, thus the wrapping // 'if' block contains a redundant (since the ref is only used on iOS) platform check - textInputRef.current.setSelection(positionSnapshot, positionSnapshot); + textInputRef.current?.setSelection(positionSnapshot, positionSnapshot); }); } }, @@ -424,8 +430,8 @@ function ComposerWithSuggestions( ); const onSelectionChange = useCallback( - (e) => { - if (textInputRef.current && textInputRef.current.isFocused() && suggestionsRef.current.onSelectionChange(e)) { + (e: NativeSyntheticEvent) => { + if (textInputRef.current?.isFocused() && suggestionsRef.current?.onSelectionChange?.(e)) { return; } @@ -451,7 +457,7 @@ function ComposerWithSuggestions( /** * Focus the composer text input - * @param {Boolean} [shouldDelay=false] Impose delay before focusing the composer + * @param [shouldDelay=false] Impose delay before focusing the composer * @memberof ReportActionCompose */ const focus = useCallback((shouldDelay = false) => { @@ -475,12 +481,12 @@ function ComposerWithSuggestions( */ const checkComposerVisibility = useCallback(() => { // Checking whether the screen is focused or not, helps avoid `modal.isVisible` false when popups are closed, even if the modal is opened. - const isComposerCoveredUp = !isFocused || EmojiPickerActions.isEmojiPickerVisible() || isMenuVisible || modal.isVisible || modal.willAlertModalBecomeVisible; + const isComposerCoveredUp = !isFocused || EmojiPickerActions.isEmojiPickerVisible() || isMenuVisible || !!modal?.isVisible || modal?.willAlertModalBecomeVisible; return !isComposerCoveredUp; }, [isMenuVisible, modal, isFocused]); const focusComposerOnKeyPress = useCallback( - (e) => { + (e: KeyboardEvent) => { const isComposerVisible = checkComposerVisibility(); if (!isComposerVisible) { return; @@ -491,7 +497,7 @@ function ComposerWithSuggestions( } // if we're typing on another input/text area, do not focus - if (['INPUT', 'TEXTAREA'].includes(e.target.nodeName)) { + if (['INPUT', 'TEXTAREA'].includes((e.target as Element)?.nodeName)) { return; } @@ -527,17 +533,17 @@ function ComposerWithSuggestions( }; }, [focusComposerOnKeyPress, navigation, setUpComposeFocusManager]); - const prevIsModalVisible = usePrevious(modal.isVisible); + const prevIsModalVisible = usePrevious(modal?.isVisible); const prevIsFocused = usePrevious(isFocused); useEffect(() => { - if (modal.isVisible && !prevIsModalVisible) { + if (modal?.isVisible && !prevIsModalVisible) { // eslint-disable-next-line no-param-reassign isNextModalWillOpenRef.current = false; } // We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused. // We avoid doing this on native platforms since the software keyboard popping // open creates a jarring and broken UX. - if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal.isVisible && isFocused && (prevIsModalVisible || !prevIsFocused))) { + if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal?.isVisible && isFocused && (!!prevIsModalVisible || !prevIsFocused))) { return; } @@ -546,11 +552,11 @@ function ComposerWithSuggestions( return; } focus(true); - }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef, shouldAutoFocus]); + }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal?.isVisible, isNextModalWillOpenRef, shouldAutoFocus]); useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit - updateMultilineInputRange(textInputRef.current, shouldAutoFocus); + updateMultilineInputRange(textInputRef.current, !!shouldAutoFocus); if (value.length === 0) { return; @@ -568,7 +574,7 @@ function ComposerWithSuggestions( focus, replaceSelectionWithText, prepareCommentAndResetComposer, - isFocused: () => textInputRef.current.isFocused(), + isFocused: () => !!textInputRef.current?.isFocused(), }), [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); @@ -582,7 +588,7 @@ function ComposerWithSuggestions( { + onLayout={(e: LayoutChangeEvent) => { const composerLayoutHeight = e.nativeEvent.layout.height; if (composerHeight === composerLayoutHeight) { return; @@ -625,7 +631,7 @@ function ComposerWithSuggestions( void; onSelectionChange?: (event: NativeSyntheticEvent) => void; - triggerHotkeyActions: (event: KeyboardEvent) => void; + triggerHotkeyActions: (event: KeyboardEvent) => boolean | undefined; updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; getSuggestions: () => Mention[] | Emoji[]; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index afee06dbc8ff..3f883d46a995 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -10,7 +10,7 @@ import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './t * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. */ -function SilentCommentUpdater({comment = '', commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { +function SilentCommentUpdater({comment, commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { const prevCommentProp = usePrevious(comment); const prevReportId = usePrevious(report?.reportID); const {preferredLocale} = useLocalize(); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 2500af40c7c6..0968a61c3abb 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef} from 'react'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; import {DragAndDropContext} from '@components/DragAndDrop/Provider'; import usePrevious from '@hooks/usePrevious'; @@ -19,7 +19,7 @@ type SuggestionProps = { selection: Selection; setSelection: (newSelection: Selection) => void; updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; - measureParentContainer: () => void; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; isComposerFullSize: boolean; isComposerFocused?: boolean; resetKeyboardInput?: () => void; From 35c7989f0a50998748982e6a4722ea2953616035 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 24 Jan 2024 18:41:13 +0700 Subject: [PATCH 052/297] remove cached file when delete comment --- src/ONYXKEYS.ts | 4 ++ .../AttachmentCarousel/CarouselItem.js | 1 + .../Attachments/AttachmentView/index.js | 17 +++++++- src/components/PDFView/index.native.js | 6 ++- src/libs/actions/CachedPDFPaths.ts | 39 +++++++++++++++++++ src/libs/actions/Report.ts | 2 + 6 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 src/libs/actions/CachedPDFPaths.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9693c907a5fe..e8caede964e6 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -249,6 +249,9 @@ const ONYXKEYS = { /** Indicates whether an forced upgrade is required */ UPDATE_REQUIRED: 'updateRequired', + // Paths of PDF file that has been cached during one session + CACHED_PDF_PATHS: 'cachedPDFPaths', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -446,6 +449,7 @@ type OnyxValues = { [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; [ONYXKEYS.UPDATE_REQUIRED]: boolean; + [ONYXKEYS.CACHED_PDF_PATHS]: Record; // Collections [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index 5552f15320f3..53d72be49a47 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -110,6 +110,7 @@ function CarouselItem({item, index, activeIndex, isSingleItem, onPress}) { carouselActiveItemIndex={activeIndex} onPress={onPress} transactionID={item.transactionID} + reportActionID={item.reportActionID} /> diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index b0060afdb813..1a6f4d92c0e8 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -15,6 +15,7 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import compose from '@libs/compose'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -47,6 +48,9 @@ const propTypes = { /** The id of the transaction related to the attachment */ // eslint-disable-next-line react/no-unused-prop-types transactionID: PropTypes.string, + + /** The id of the report action related to the attachment */ + reportActionID: PropTypes.string, }; const defaultProps = { @@ -57,6 +61,7 @@ const defaultProps = { containerStyles: [], isWorkspaceAvatar: false, transactionID: '', + reportActionID: '', }; function AttachmentView({ @@ -79,6 +84,7 @@ function AttachmentView({ isWorkspaceAvatar, fallbackSource, transaction, + reportActionID, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -127,6 +133,15 @@ function AttachmentView({ if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; + const onPDFLoadComplete = (path) => { + if (isUsedInCarousel && reportActionID) { + CachedPDFPaths.add(reportActionID, path); + } + if (!loadComplete) { + setLoadComplete(true); + } + }; + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -143,7 +158,7 @@ function AttachmentView({ onPress={onPress} onScaleChanged={onScaleChanged} onToggleKeyboard={onToggleKeyboard} - onLoadComplete={() => !loadComplete && setLoadComplete(true)} + onLoadComplete={onPDFLoadComplete} errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index cbe7f0e4608e..08f2eb2c6984 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -118,14 +118,16 @@ class PDFView extends Component { /** * After the PDF is successfully loaded hide PDFPasswordForm and the loading * indicator. + * @param {Number} numberOfPages + * @param {Number} path - Path to cache location */ - finishPDFLoad() { + finishPDFLoad(numberOfPages, path) { this.setState({ shouldRequestPassword: false, shouldShowLoadingIndicator: false, successToLoadPDF: true, }); - this.props.onLoadComplete(); + this.props.onLoadComplete(path); } renderPDFView() { diff --git a/src/libs/actions/CachedPDFPaths.ts b/src/libs/actions/CachedPDFPaths.ts new file mode 100644 index 000000000000..28626dc34856 --- /dev/null +++ b/src/libs/actions/CachedPDFPaths.ts @@ -0,0 +1,39 @@ +import {exists, unlink} from 'react-native-fs'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +let pdfPaths: Record = {}; +Onyx.connect({ + key: ONYXKEYS.CACHED_PDF_PATHS, + callback: (val) => { + pdfPaths = val ?? {}; + }, +}); + +function add(reportActionID: string, path: string): Promise { + return Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {[reportActionID]: path}); +} + +function clear(path: string): Promise { + if (!path) { + return Promise.resolve(); + } + return new Promise((resolve) => { + exists(path).then((exist) => { + if (!exist) { + resolve(); + } + return unlink(path); + }); + }); +} + +function clearByKey(reportActionID: string) { + clear(pdfPaths[reportActionID] ?? '').then(() => Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {[reportActionID]: null})); +} + +function clearAll() { + Promise.all(Object.values(pdfPaths).map(clear)).then(() => Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {})); +} + +export {add, clearByKey, clearAll}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 228b88d194ba..753831470c66 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -40,6 +40,7 @@ import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/Rep import type ReportAction from '@src/types/onyx/ReportAction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import * as CachedPDFPaths from './CachedPDFPaths'; import * as Modal from './Modal'; import * as Session from './Session'; import * as Welcome from './Welcome'; @@ -1223,6 +1224,7 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { reportActionID, }; + CachedPDFPaths.clearByKey(reportActionID); API.write('DeleteComment', parameters, {optimisticData, successData, failureData}); } From 223fdd804f52f0adf9eb5891f8cd00da0dd91726 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 12:51:51 +0100 Subject: [PATCH 053/297] fix: wip --- src/components/Composer/index.tsx | 2 +- src/components/Composer/types.ts | 2 +- .../ComposerWithSuggestions.tsx | 10 ++++++---- .../ReportActionCompose/SilentCommentUpdater/index.tsx | 7 +++---- .../ReportActionCompose/SilentCommentUpdater/types.ts | 3 --- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index e78e0c2a4039..a23a176c5bcd 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -300,7 +300,7 @@ function Composer( }, []); const handleKeyPress = useCallback( - (e: NativeSyntheticEvent) => { + (e: NativeSyntheticEvent & KeyboardEvent) => { // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed if (!onKeyPress || isEnterWhileComposition(e as unknown as KeyboardEvent)) { return; diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index d8d88970ea78..6e486771c9aa 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -74,7 +74,7 @@ type ComposerProps = { /** Whether the sull composer is open */ isComposerFullSize?: boolean; - onKeyPress?: (event: NativeSyntheticEvent) => void; + onKeyPress?: (event: KeyboardEvent & NativeSyntheticEvent) => void; onFocus?: (event: NativeSyntheticEvent) => void; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index cb5f2b52ffbc..034affefd747 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -2,7 +2,7 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefAttributes, RefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData} from 'react-native'; +import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -73,7 +73,7 @@ type ComposerWithSuggestionsProps = { report: OnyxEntry; reportActions: OnyxTypes.ReportAction[] | undefined; onFocus: () => void; - onBlur: (event: FocusEvent) => void; + onBlur: (event: NativeSyntheticEvent) => void; onValueChange: (value: string) => void; isComposerFullSize: boolean; isMenuVisible: boolean; @@ -222,6 +222,7 @@ function ComposerWithSuggestions( */ const setTextInputRef = useCallback( (el: TextInput) => { + // @ts-expect-error need to reassign this ref ReportActionComposeFocusManager.composerRef.current = el; textInputRef.current = el; if (typeof animatedRef === 'function') { @@ -373,7 +374,7 @@ function ComposerWithSuggestions( ); const triggerHotkeyActions = useCallback( - (e: KeyboardEvent) => { + (e: KeyboardEvent & NativeSyntheticEvent) => { if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } @@ -537,6 +538,7 @@ function ComposerWithSuggestions( const prevIsFocused = usePrevious(isFocused); useEffect(() => { if (modal?.isVisible && !prevIsModalVisible) { + // @ts-expect-error need to reassign this ref // eslint-disable-next-line no-param-reassign isNextModalWillOpenRef.current = false; } @@ -682,7 +684,7 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, canEvict: false, initWithStoredValues: false, }, diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index 3f883d46a995..626938c235da 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -10,14 +10,14 @@ import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './t * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. */ -function SilentCommentUpdater({comment, commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { +function SilentCommentUpdater({comment = '', commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { const prevCommentProp = usePrevious(comment); const prevReportId = usePrevious(report?.reportID); const {preferredLocale} = useLocalize(); const prevPreferredLocale = usePrevious(preferredLocale); useEffect(() => { - updateComment(comment); + updateComment(comment ?? null); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); @@ -32,7 +32,7 @@ function SilentCommentUpdater({comment, commentRef, report, value, updateComment return; } - updateComment(comment); + updateComment(comment ?? null); }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report?.reportID, updateComment, value, commentRef]); return null; @@ -43,6 +43,5 @@ SilentCommentUpdater.displayName = 'SilentCommentUpdater'; export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, - initialValue: '', }, })(SilentCommentUpdater); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts index 60924f24099b..591ee43ce6cd 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -21,9 +21,6 @@ type SilentCommentUpdaterProps = { /** The ref of the comment */ commentRef: React.RefObject; - - /** The comment of the report */ - commnet: string; } & SilentCommentUpdaterOnyxProps; export type {SilentCommentUpdaterProps, SilentCommentUpdaterOnyxProps}; From 05bb0a591c9401ae62f15833afd801237e6af3ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 16:20:44 +0100 Subject: [PATCH 054/297] fix: finished migrating ComposerWithSuggestions, removed unused files, start migrating ReportTypeingIndicator --- src/components/Composer/index.tsx | 3 +- src/components/Composer/types.ts | 10 +- src/components/MentionSuggestions.tsx | 3 +- src/libs/E2E/client.ts | 3 + .../ComposerWithSuggestions.tsx | 37 ++--- .../composerWithSuggestionsProps.js | 126 ------------------ .../ComposerWithSuggestions/index.e2e.tsx | 18 +-- .../ReportActionCompose.tsx | 9 +- .../ReportActionCompose/suggestionProps.js | 39 ------ ...Indicator.js => ReportTypingIndicator.tsx} | 0 src/types/modules/react-native.d.ts | 5 - 11 files changed, 46 insertions(+), 207 deletions(-) delete mode 100644 src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js delete mode 100644 src/pages/home/report/ReportActionCompose/suggestionProps.js rename src/pages/home/report/{ReportTypingIndicator.js => ReportTypingIndicator.tsx} (100%) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index a23a176c5bcd..493b37ff3a31 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -300,11 +300,12 @@ function Composer( }, []); const handleKeyPress = useCallback( - (e: NativeSyntheticEvent & KeyboardEvent) => { + (e: NativeSyntheticEvent) => { // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed if (!onKeyPress || isEnterWhileComposition(e as unknown as KeyboardEvent)) { return; } + onKeyPress(e); }, [onKeyPress], diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 6e486771c9aa..9565eaf6208f 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,4 +1,4 @@ -import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; type TextSelection = { start: number; @@ -74,14 +74,8 @@ type ComposerProps = { /** Whether the sull composer is open */ isComposerFullSize?: boolean; - onKeyPress?: (event: KeyboardEvent & NativeSyntheticEvent) => void; - - onFocus?: (event: NativeSyntheticEvent) => void; - - onBlur?: (event: NativeSyntheticEvent) => void; - /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; -}; +} & TextInputProps; export type {TextSelection, ComposerProps}; diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 99930f995a3a..23040a242807 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -1,4 +1,5 @@ import React, {useCallback} from 'react'; +import type {MeasureInWindowOnSuccessCallback} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -43,7 +44,7 @@ type MentionSuggestionsProps = { isMentionPickerLarge: boolean; /** Measures the parent container's position and dimensions. */ - measureParentContainer: () => void; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; }; /** diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 472567cc6c1d..7dc3ad4073b9 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -11,6 +11,9 @@ type TestResult = { type TestConfig = { name: string; + reportScreen?: { + autoFocus?: boolean; + }; }; type NativeCommandPayload = { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 034affefd747..c02bda08cac7 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -2,7 +2,15 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefAttributes, RefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; +import type { + LayoutChangeEvent, + MeasureInWindowOnSuccessCallback, + NativeSyntheticEvent, + TextInput, + TextInputFocusEventData, + TextInputKeyPressEventData, + TextInputSelectionChangeEventData, +} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -42,7 +50,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {defaultProps, propTypes} from './composerWithSuggestionsProps'; type SyncSelection = { position: number; @@ -126,7 +133,7 @@ function ComposerWithSuggestions( { // Onyx modal, - preferredSkinTone, + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, parentReportActions, numberOfLines, @@ -152,7 +159,7 @@ function ComposerWithSuggestions( setIsCommentEmpty, handleSendMessage, shouldShowComposeInput, - measureParentContainer, + measureParentContainer = () => {}, listHeight, isScrollLikelyLayoutTriggered, raiseIsScrollLikelyLayoutTriggered, @@ -164,7 +171,7 @@ function ComposerWithSuggestions( // For testing children, }: ComposerWithSuggestionsProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); @@ -374,32 +381,33 @@ function ComposerWithSuggestions( ); const triggerHotkeyActions = useCallback( - (e: KeyboardEvent & NativeSyntheticEvent) => { - if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { + (event: NativeSyntheticEvent) => { + const webEvent = event as unknown as KeyboardEvent; + if (!webEvent || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } - if (suggestionsRef.current?.triggerHotkeyActions(e)) { + if (suggestionsRef.current?.triggerHotkeyActions(webEvent)) { return; } // Submit the form when Enter is pressed - if (e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !e.shiftKey) { - e.preventDefault(); + if (webEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !webEvent.shiftKey) { + webEvent.preventDefault(); handleSendMessage(); } // Trigger the edit box for last sent message if ArrowUp is pressed and the comment is empty and Chronos is not in the participants const valueLength = valueRef.current.length; if ( - e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && + webEvent.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current && 'selectionStart' in textInputRef.current && textInputRef.current?.selectionStart === 0 && valueLength === 0 && !ReportUtils.chatIncludesChronos(report) ) { - e.preventDefault(); + webEvent.preventDefault(); const lastReportAction = [...(reportActions ?? []), parentReportAction].find( (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action), ); @@ -568,7 +576,6 @@ function ComposerWithSuggestions( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useImperativeHandle( ref, () => ({ @@ -660,8 +667,6 @@ function ComposerWithSuggestions( ); } -ComposerWithSuggestions.propTypes = propTypes; -ComposerWithSuggestions.defaultProps = defaultProps; ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); @@ -689,3 +694,5 @@ export default withOnyx {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index cbbd1758c9cb..87352981ba9a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -1,6 +1,8 @@ -import _ from 'lodash'; -import React, {useEffect} from 'react'; +import type {ForwardedRef, RefObject} from 'react'; +import React, {forwardRef, useEffect} from 'react'; import E2EClient from '@libs/E2E/client'; +import type {ComposerRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; +import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions'; import ComposerWithSuggestions from './ComposerWithSuggestions'; let rerenderCount = 0; @@ -14,20 +16,20 @@ function IncrementRenderCount() { return null; } -const ComposerWithSuggestionsE2e = React.forwardRef((props, ref) => { +function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { // Eventually Auto focus on e2e tests useEffect(() => { - if (_.get(E2EClient.getCurrentActiveTestConfig(), 'reportScreen.autoFocus', false) === false) { + if ((E2EClient.getCurrentActiveTestConfig()?.reportScreen?.autoFocus ?? false) === false) { return; } // We need to wait for the component to be mounted before focusing setTimeout(() => { - if (!ref || !ref.current) { + if (!(ref as RefObject)?.current) { return; } - ref.current.focus(true); + (ref as RefObject).current?.focus(true); }, 1); }, [ref]); @@ -44,9 +46,9 @@ const ComposerWithSuggestionsE2e = React.forwardRef((props, ref) => { ); -}); +} ComposerWithSuggestionsE2e.displayName = 'ComposerWithSuggestionsE2e'; -export default ComposerWithSuggestionsE2e; +export default forwardRef(ComposerWithSuggestionsE2e); export {getRerenderCount, resetRerenderCount}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index d7e02fe29fcd..70c5f61d7725 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,7 +1,7 @@ import {PortalHost} from '@gorhom/portal'; import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -123,7 +123,7 @@ function ReportActionCompose({ const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); const animatedRef = useAnimatedRef(); - const actionButtonRef = useRef(null); + const actionButtonRef = useRef(null); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; /** * Updates the Highlight state of the composer @@ -300,12 +300,13 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((event: FocusEvent) => { + const onBlur = useCallback((event: NativeSyntheticEvent) => { + const webEvent = event as unknown as FocusEvent; setIsFocused(false); if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); } - if (event.relatedTarget && event.relatedTarget === actionButtonRef.current) { + if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) { isKeyboardVisibleWhenShowingModalRef.current = true; } }, []); diff --git a/src/pages/home/report/ReportActionCompose/suggestionProps.js b/src/pages/home/report/ReportActionCompose/suggestionProps.js deleted file mode 100644 index 62c29f3d418e..000000000000 --- a/src/pages/home/report/ReportActionCompose/suggestionProps.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; - -const baseProps = { - /** The current input value */ - value: PropTypes.string.isRequired, - - /** Callback to update the current input value */ - setValue: PropTypes.func.isRequired, - - /** The current selection value */ - selection: PropTypes.shape({ - start: PropTypes.number.isRequired, - end: PropTypes.number.isRequired, - }).isRequired, - - /** Callback to update the current selection */ - setSelection: PropTypes.func.isRequired, - - /** Whether the composer is expanded */ - isComposerFullSize: PropTypes.bool.isRequired, - - /** Callback to update the comment draft */ - updateComment: PropTypes.func.isRequired, - - /** Meaures the parent container's position and dimensions. */ - measureParentContainer: PropTypes.func.isRequired, - - /** Report composer focus state */ - isComposerFocused: PropTypes.bool, -}; - -const implementationBaseProps = { - /** Whether to use the small or the big suggestion picker */ - isAutoSuggestionPickerLarge: PropTypes.bool.isRequired, - - ...baseProps, -}; - -export {baseProps, implementationBaseProps}; diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.tsx similarity index 100% rename from src/pages/home/report/ReportTypingIndicator.js rename to src/pages/home/report/ReportTypingIndicator.tsx diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index aaa7058737ae..7313a28984fc 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -279,14 +279,9 @@ declare module 'react-native' { * Extracted from react-native-web, packages/react-native-web/src/exports/TextInput/types.js */ interface WebTextInputProps extends WebSharedProps { - dir?: 'auto' | 'ltr' | 'rtl'; disabled?: boolean; - enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'; - readOnly?: boolean; } interface TextInputProps extends WebTextInputProps { - // TODO: remove once the app is updated to RN 0.73 - smartInsertDelete?: boolean; isFullComposerAvailable?: boolean; } From cf84b3b817a85d6baa5c28e2913068e2d487b260 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 17:47:16 +0100 Subject: [PATCH 055/297] fix: move ReportTypingIndicator to TS --- .../home/report/ReportTypingIndicator.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index 785f1e3f6a1e..f7f135e7374e 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -1,7 +1,6 @@ -import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import Text from '@components/Text'; import TextWithEllipsis from '@components/TextWithEllipsis'; import useLocalize from '@hooks/useLocalize'; @@ -9,28 +8,30 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportUserIsTyping} from '@src/types/onyx'; -const propTypes = { +type ReportTypingIndicatorOnyxProps = { /** Key-value pairs of user accountIDs/logins and whether or not they are typing. Keys are accountIDs or logins. */ - userTypingStatuses: PropTypes.objectOf(PropTypes.bool), + userTypingStatuses: OnyxEntry; }; -const defaultProps = { - userTypingStatuses: {}, -}; +type ReportTypingIndicatorProps = { + // eslint-disable-next-line react/no-unused-prop-types + reportID: string; +} & ReportTypingIndicatorOnyxProps; -function ReportTypingIndicator({userTypingStatuses}) { +function ReportTypingIndicator({userTypingStatuses}: ReportTypingIndicatorProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); + const usersTyping = useMemo(() => Object.keys(userTypingStatuses ?? {}).filter((loginOrAccountID) => userTypingStatuses?.[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)); // If we are offline, the user typing statuses are not up-to-date so do not show them - if (isOffline || !firstUserTyping) { + if (!!isOffline || !firstUserTyping) { return null; } @@ -40,6 +41,7 @@ function ReportTypingIndicator({userTypingStatuses}) { if (usersTyping.length === 1) { return ( ({ userTypingStatuses: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, - initialValue: {}, }, })(ReportTypingIndicator); From 87f22f7e925b169a76d74e9a3eed6643283306c9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 18:26:51 +0100 Subject: [PATCH 056/297] fix: migrate ParticipanLocalTime and ReportDropUI --- ...tLocalTime.js => ParticipantLocalTime.tsx} | 35 ++++++++++--------- .../{ReportDropUI.js => ReportDropUI.tsx} | 9 ++--- .../home/report/ReportTypingIndicator.tsx | 2 +- src/types/onyx/IOU.ts | 2 ++ 4 files changed, 25 insertions(+), 23 deletions(-) rename src/pages/home/report/{ParticipantLocalTime.js => ParticipantLocalTime.tsx} (66%) rename src/pages/home/report/{ReportDropUI.js => ReportDropUI.tsx} (87%) diff --git a/src/pages/home/report/ParticipantLocalTime.js b/src/pages/home/report/ParticipantLocalTime.tsx similarity index 66% rename from src/pages/home/report/ParticipantLocalTime.js rename to src/pages/home/report/ParticipantLocalTime.tsx index 1992953c959e..c8bbaf031e32 100644 --- a/src/pages/home/report/ParticipantLocalTime.js +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -1,36 +1,39 @@ -import lodashGet from 'lodash/get'; import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; -import participantPropTypes from '@components/participantPropTypes'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import Timers from '@libs/Timers'; import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; -const propTypes = { +type Locales = DeepValueOf; + +type ParticipantLocalTimeProps = { /** Personal details of the participant */ - participant: participantPropTypes.isRequired, + participant: PersonalDetails; - ...withLocalizePropTypes, + preferredLocale: Locales; }; -function getParticipantLocalTime(participant, preferredLocale) { - const reportRecipientTimezone = lodashGet(participant, 'timezone', CONST.DEFAULT_TIME_ZONE); - const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, null, reportRecipientTimezone.selected); +function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locales) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; + const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, undefined, reportRecipientTimezone.selected); const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale); - const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone); - const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone); + const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone.toDateString()); + const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone.toDateString()); if (reportRecipientDay !== currentUserDay) { return `${DateUtils.formatToLocalTime(reportTimezone)} ${reportRecipientDay}`; } return `${DateUtils.formatToLocalTime(reportTimezone)}`; } -function ParticipantLocalTime(props) { +function ParticipantLocalTime({participant, preferredLocale}: ParticipantLocalTimeProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); - const {participant, preferredLocale, translate} = props; const [localTime, setLocalTime] = useState(() => getParticipantLocalTime(participant, preferredLocale)); useEffect(() => { @@ -44,7 +47,8 @@ function ParticipantLocalTime(props) { }; }, [participant, preferredLocale]); - const reportRecipientDisplayName = lodashGet(props, 'participant.firstName') || lodashGet(props, 'participant.displayName'); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportRecipientDisplayName = participant.firstName || participant.displayName; if (!reportRecipientDisplayName) { return null; @@ -65,7 +69,6 @@ function ParticipantLocalTime(props) { ); } -ParticipantLocalTime.propTypes = propTypes; ParticipantLocalTime.displayName = 'ParticipantLocalTime'; -export default withLocalize(ParticipantLocalTime); +export default ParticipantLocalTime; diff --git a/src/pages/home/report/ReportDropUI.js b/src/pages/home/report/ReportDropUI.tsx similarity index 87% rename from src/pages/home/report/ReportDropUI.js rename to src/pages/home/report/ReportDropUI.tsx index c1c3b8e506ab..d147d0c0c03d 100644 --- a/src/pages/home/report/ReportDropUI.js +++ b/src/pages/home/report/ReportDropUI.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; @@ -8,12 +7,11 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -const propTypes = { +type ReportDropUIProps = { /** Callback to execute when a file is dropped. */ - onDrop: PropTypes.func.isRequired, + onDrop: () => void; }; - -function ReportDropUI({onDrop}) { +function ReportDropUI({onDrop}: ReportDropUIProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); return ( @@ -33,6 +31,5 @@ function ReportDropUI({onDrop}) { } ReportDropUI.displayName = 'ReportDropUI'; -ReportDropUI.propTypes = propTypes; export default ReportDropUI; diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index f7f135e7374e..f62db8bf0337 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -16,7 +16,7 @@ type ReportTypingIndicatorOnyxProps = { }; type ReportTypingIndicatorProps = { - // eslint-disable-next-line react/no-unused-prop-types + // eslint-disable-next-line react/no-unused-prop-types -- This is used by withOnyx reportID: string; } & ReportTypingIndicatorOnyxProps; diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index a89b0d4530ef..220af7005c45 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -23,3 +23,5 @@ type IOU = { }; export default IOU; + +export type {Participant}; From 63a63ad6df7a87ceeb99de66e8316200d30721cf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 09:36:42 +0100 Subject: [PATCH 057/297] fix: typecheck --- src/components/LocaleContextProvider.tsx | 2 +- src/pages/home/report/ParticipantLocalTime.tsx | 12 +++++------- src/pages/home/report/ReportDropUI.tsx | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 7313bb4aa7bb..25b468181b87 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -132,4 +132,4 @@ Provider.displayName = 'withOnyx(LocaleContextProvider)'; export {Provider as LocaleContextProvider, LocaleContext}; -export type {LocaleContextProps}; +export type {LocaleContextProps, Locale}; diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index c8bbaf031e32..5d90659fb96a 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -5,24 +5,22 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import Timers from '@libs/Timers'; +import type {Locale} from '@src/components/LocaleContextProvider'; import CONST from '@src/CONST'; import type {PersonalDetails} from '@src/types/onyx'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; - -type Locales = DeepValueOf; type ParticipantLocalTimeProps = { /** Personal details of the participant */ participant: PersonalDetails; - preferredLocale: Locales; + preferredLocale?: Locale; }; -function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locales) { +function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale | undefined) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; - const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, undefined, reportRecipientTimezone.selected); - const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale); + const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); + const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone.toDateString()); const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone.toDateString()); if (reportRecipientDay !== currentUserDay) { diff --git a/src/pages/home/report/ReportDropUI.tsx b/src/pages/home/report/ReportDropUI.tsx index d147d0c0c03d..fad58d60bbfa 100644 --- a/src/pages/home/report/ReportDropUI.tsx +++ b/src/pages/home/report/ReportDropUI.tsx @@ -9,7 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; type ReportDropUIProps = { /** Callback to execute when a file is dropped. */ - onDrop: () => void; + onDrop: (event: DragEvent) => void; }; function ReportDropUI({onDrop}: ReportDropUIProps) { const styles = useThemeStyles(); From 1225d237f1123cb89b8e29cdc36a022042e1ad1e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 13:47:27 +0100 Subject: [PATCH 058/297] fix: minor fixes --- src/pages/home/report/ParticipantLocalTime.tsx | 5 +++-- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- src/pages/home/report/ReportActionCompose/SendButton.tsx | 3 ++- .../home/report/ReportActionCompose/SuggestionMention.tsx | 6 ++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index 5d90659fb96a..e97fad8f20f0 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -13,11 +13,12 @@ type ParticipantLocalTimeProps = { /** Personal details of the participant */ participant: PersonalDetails; + /** The user's preferred locale e.g. 'en', 'es-ES' */ preferredLocale?: Locale; }; function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale | undefined) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); @@ -45,7 +46,7 @@ function ParticipantLocalTime({participant, preferredLocale}: ParticipantLocalTi }; }, [participant, preferredLocale]); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null const reportRecipientDisplayName = participant.firstName || participant.displayName; if (!reportRecipientDisplayName) { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8c09f53d3b27..96d2e1a26f6b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -209,7 +209,7 @@ function ComposerWithSuggestions( const syncSelectionWithOnChangeTextRef = useRef(null); - const suggestions = suggestionsRef.current?.getSuggestions(); + const suggestions = suggestionsRef.current?.getSuggestions() ?? (() => []); const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions?.length ?? 0); diff --git a/src/pages/home/report/ReportActionCompose/SendButton.tsx b/src/pages/home/report/ReportActionCompose/SendButton.tsx index b6c6200fc7c0..453ee9310f46 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.tsx +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -24,7 +24,8 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP const {translate} = useLocalize(); const Tap = Gesture.Tap() - .enabled(!isDisabledProp) + // @ts-expect-error Enabled require argument but when passing something button is not working + .enabled() .onEnd(() => { handleSendMessage(); }); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index d5a3a8983467..02bcd27093e7 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -145,8 +145,10 @@ function SuggestionMention( }); const sortedPersonalDetails = filteredPersonalDetails.sort((a, b) => { - const nameA = a?.displayName ?? a?.login ?? ''; - const nameB = b?.displayName ?? b?.login ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null + const nameA = a?.displayName || a?.login || ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null + const nameB = b?.displayName || b?.login || ''; if (nameA < nameB) { return -1; From 4f907d25135d3bdc12e7a5b7971e1d71c69b558a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 13:55:20 +0100 Subject: [PATCH 059/297] fix: removed unused export --- src/types/onyx/IOU.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index 220af7005c45..a89b0d4530ef 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -23,5 +23,3 @@ type IOU = { }; export default IOU; - -export type {Participant}; From 6981075dff4b466e784ee280878d6b39eb35a1c4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Jan 2024 16:07:20 +0700 Subject: [PATCH 060/297] resolve comments --- src/libs/actions/CachedPDFPaths.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libs/actions/CachedPDFPaths.ts b/src/libs/actions/CachedPDFPaths.ts index 28626dc34856..4eefb9a7982e 100644 --- a/src/libs/actions/CachedPDFPaths.ts +++ b/src/libs/actions/CachedPDFPaths.ts @@ -2,6 +2,10 @@ import {exists, unlink} from 'react-native-fs'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +/* + * We need to save the paths of PDF files so we can delete them later. + * This is to remove the cached PDFs when an attachment is deleted or the user logs out. + */ let pdfPaths: Record = {}; Onyx.connect({ key: ONYXKEYS.CACHED_PDF_PATHS, @@ -11,6 +15,9 @@ Onyx.connect({ }); function add(reportActionID: string, path: string): Promise { + if (pdfPaths[reportActionID]) { + return Promise.resolve(); + } return Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {[reportActionID]: path}); } From 5c5b0ab9df2e4193c11e9218de31ce087c894360 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 29 Jan 2024 11:07:00 +0700 Subject: [PATCH 061/297] change params order --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 2de0ccef71b0..4b90d5fd33d4 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -377,7 +377,7 @@ function buildOnyxDataForMoneyRequest( needsToBeManuallySubmitted = true, ) { const isScanRequest = TransactionUtils.isScanRequest(transaction); - const outstandingChildRequest = getOutstandingChildRequest(needsToBeManuallySubmitted, policy, iouReport); + const outstandingChildRequest = getOutstandingChildRequest(policy, iouReport, needsToBeManuallySubmitted); const optimisticData = [ { // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page From 56824378197d9893f86bab6568db2eb70d58ed2d Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 30 Jan 2024 01:31:35 +0700 Subject: [PATCH 062/297] fix variable cap --- src/components/CustomStatusBarAndBackground/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index fa1ab2362151..33a9003d1f64 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -60,9 +60,9 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack // Updates the status bar style and background color depending on the current route and theme // This callback is triggered everytime the route changes or the theme changes const updateStatusBarStyle = useCallback( - (listenerId?: number) => { + (listenerID?: number) => { // Check if this function is either called through the current navigation listener or the general useEffect which listens for theme changes. - if (listenerId !== undefined && listenerId !== listenerCount.current) { + if (listenerID !== undefined && listenerID !== listenerCount.current) { return; } @@ -125,8 +125,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack // Add navigation state listeners to update the status bar every time the route changes // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly - const listenerId = ++listenerCount.current; - const listener = () => updateStatusBarStyle(listenerId); + const listenerID = ++listenerCount.current; + const listener = () => updateStatusBarStyle(listenerID); navigationRef.addListener('state', listener); return () => navigationRef.removeListener('state', listener); From 0229c977b32f2f6b1c054098ae8cb8308ce470bd Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 30 Jan 2024 10:52:31 +0700 Subject: [PATCH 063/297] fix conflict main --- src/pages/ProfilePage.js | 47 ++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 590a4635f9e5..f20055190e3e 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -5,7 +5,6 @@ import React, {useEffect} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import AttachmentModal from '@components/AttachmentModal'; import AutoUpdateTime from '@components/AutoUpdateTime'; import Avatar from '@components/Avatar'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -101,7 +100,6 @@ function ProfilePage(props) { const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details); const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar()); const fallbackIcon = lodashGet(details, 'fallbackIcon', ''); - const originalFileName = lodashGet(details, 'originalFileName', ''); const login = lodashGet(details, 'login', ''); const timezone = lodashGet(details, 'timezone', {}); @@ -122,6 +120,9 @@ function ProfilePage(props) { const hasMinimumDetails = !_.isEmpty(details.avatar); const isLoading = lodashGet(details, 'isLoading', false) || _.isEmpty(details); + // If the API returns an error for some reason there won't be any details and isLoading will get set to false, so we want to show a blocking screen + const shouldShowBlockingView = !hasMinimumDetails && !isLoading; + const statusEmojiCode = lodashGet(details, 'status.emojiCode', ''); const statusText = lodashGet(details, 'status.text', ''); const hasStatus = !!statusEmojiCode; @@ -141,7 +142,7 @@ function ProfilePage(props) { return ( - + Navigation.goBack(navigateBackTo)} @@ -150,32 +151,22 @@ function ProfilePage(props) { {hasMinimumDetails && ( - Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))} + accessibilityLabel={props.translate('common.profile')} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} > - {({show}) => ( - - - - - - )} - + + + + {Boolean(displayName) && ( Date: Tue, 30 Jan 2024 10:57:10 +0100 Subject: [PATCH 064/297] fix: typecheck --- .../MoneyRequestConfirmationList.tsx | 39 ++++++++++++++----- src/libs/OptionsListUtils.ts | 19 +++++++-- src/types/onyx/index.ts | 3 +- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 3712e928a5e1..3a6dd5964741 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -28,7 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; -import type {PaymentType} from '@src/types/onyx/OriginalMessage'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {MileageRate} from '@src/types/onyx/Policy'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import ConfirmedRoute from './ConfirmedRoute'; @@ -36,6 +36,7 @@ import FormHelpMessage from './FormHelpMessage'; import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import OptionsSelector from './OptionsSelector'; +import ReceiptEmptyState from './ReceiptEmptyState'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; @@ -78,7 +79,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onConfirm?: (selectedParticipants: Participant[]) => void; /** Callback to parent modal to send money */ - onSendMoney?: (paymentMethod: PaymentType) => void; + onSendMoney?: (paymentMethod: PaymentMethodType) => void; /** Callback to inform a participant is selected */ onSelectParticipant?: (option: Participant) => void; @@ -232,7 +233,7 @@ function MoneyRequestConfirmationList({ const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(policyCategories ?? {})); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -249,7 +250,7 @@ function MoneyRequestConfirmationList({ const policyTagList = policyTag?.tags ?? {}; const policyTagListName = policyTag?.name ?? translate('common.tag'); // A flag for showing the tags field - const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledOptions(Object.values(policyTagList))); + const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledOptions(policyTagList)); // A flag for showing tax fields - tax rate and tax amount const shouldShowTax = isPolicyExpenseChat && policy?.isTaxTrackingEnabled; @@ -322,8 +323,7 @@ function MoneyRequestConfirmationList({ return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, calculatedIouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', - // TODO: Remove the assertion once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - ) as Participant[]; + ); }, [iouAmount, iouCurrencyCode], ); @@ -467,7 +467,7 @@ function MoneyRequestConfirmationList({ }; const confirm = useCallback( - (paymentMethod: PaymentType) => { + (paymentMethod: PaymentMethodType) => { if (selectedParticipantsMemo.length === 0) { return; } @@ -552,7 +552,7 @@ function MoneyRequestConfirmationList({ pressOnEnter isDisabled={shouldDisableButton} // eslint-disable-next-line @typescript-eslint/naming-convention - onPress={(_, value) => confirm(value as PaymentType)} + onPress={(_, value) => confirm(value as PaymentMethodType)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} /> @@ -612,7 +612,7 @@ function MoneyRequestConfirmationList({ )} - {(receiptData.image || receiptData.thumbnail) && ( + {receiptData.image || receiptData.thumbnail ? ( + ) : ( + // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + PolicyUtils.isPaidGroupPolicy(policy) && + !isDistanceRequest && + iouType === CONST.IOU.TYPE.REQUEST && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( + CONST.IOU.ACTION.CREATE, + iouType, + transaction?.transactionID ?? '', + reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ) + } + /> + ) )} {shouldShowSmartScanFields && ( { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 812ebb051624..a1714629c21c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -10,7 +10,20 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import type { + Beta, + Login, + PersonalDetails, + PersonalDetailsList, + Policy, + PolicyCategories, + PolicyTags, + Report, + ReportAction, + ReportActions, + Transaction, + TransactionViolation, +} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; @@ -768,7 +781,7 @@ function getEnabledCategoriesCount(options: PolicyCategories): number { /** * Verifies that there is at least one enabled option */ -function hasEnabledOptions(options: PolicyCategories): boolean { +function hasEnabledOptions(options: PolicyCategories | PolicyTags): boolean { return Object.values(options).some((option) => option.enabled); } @@ -1699,7 +1712,7 @@ function getSearchOptions(reports: Record, personalDetails: Onyx /** * Build the IOUConfirmation options for showing the payee personalDetail */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PayeePersonalDetails { +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails | EmptyObject, amountText: string): PayeePersonalDetails { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); return { text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 011242b2ba38..81bca425f78e 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -33,8 +33,7 @@ import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type {PolicyReportField, PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTags} from './PolicyTag'; -import type {PolicyTaxRates} from './PolicyTaxRates'; -import type PolicyTaxRate from './PolicyTaxRates'; +import type {PolicyTaxRate, PolicyTaxRates} from './PolicyTaxRates'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; From 2c030a3d49b2fe430e61eeb4461567db56353575 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 11:02:40 +0100 Subject: [PATCH 065/297] fix: remove unnecessary default values --- src/components/MoneyRequestConfirmationList.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 3a6dd5964741..74ac5e878395 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -187,12 +187,12 @@ function MoneyRequestConfirmationList({ payeePersonalDetails, canModifyParticipants = false, isReadOnly = false, - bankAccountRoute = '', - policyID = '', - reportID = '', - receiptPath = '', - receiptFilename = '', - transactionID = '', + bankAccountRoute, + policyID, + reportID, + receiptPath, + receiptFilename, + transactionID, mileageRate, isDistanceRequest = false, isScanRequest = false, From efa41789b32f559b5618d316be79d8b1398e53e4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 11:16:15 +0100 Subject: [PATCH 066/297] fix: typecheck --- src/libs/ComposerUtils/index.ts | 4 ++-- src/libs/EmojiUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 94bba5d0d00c..de937c0ba915 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -18,8 +18,8 @@ function insertText(text: string, selection: Selection, textToInsert: string): s * Insert a white space at given index of text * @param text - text that needs whitespace to be appended to */ -function insertWhiteSpaceAtIndex(text: string, index: number) { - return `${text.slice(0, index)} ${text.slice(index)}`; +function insertWhiteSpaceAtIndex(text: string | null, index: number) { + return `${text?.slice(0, index)} ${text?.slice(index)}`; } /** diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 7971e6147c19..ecbed9c87eff 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -370,7 +370,7 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF /** * Find all emojis in a text and replace them with their code. */ -function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); return { From 99a4110c3e4dde03061ee0872b51949b4caec0f8 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 11:58:11 +0100 Subject: [PATCH 067/297] fix: typecheck --- src/libs/E2E/tests/chatOpeningTest.e2e.ts | 3 ++- src/libs/E2E/tests/reportTypingTest.e2e.ts | 3 ++- src/libs/E2E/types.ts | 2 +- src/libs/EmojiUtils.ts | 4 ++-- .../ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx | 3 ++- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index ef380f847c3f..2e2ce76b348d 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -1,3 +1,4 @@ +import type {NativeConfig} from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; @@ -12,7 +13,7 @@ const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - const reportID = getConfigValueOrThrow('reportID', config); + const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index 4e0678aeb020..17464f7eb8d6 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -1,3 +1,4 @@ +import type {NativeConfig} from 'react-native-config'; import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; @@ -16,7 +17,7 @@ const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for typing'); - const reportID = getConfigValueOrThrow('reportID', config); + const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index 2d48813fa115..93640fbb4ce8 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -20,7 +20,7 @@ type NetworkCacheMap = Record< type TestConfig = { name: string; - [key: string]: string; + [key: string]: string | {autoFocus: boolean}; }; export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig}; diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index ecbed9c87eff..12319e342fb8 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -371,11 +371,11 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF * Find all emojis in a text and replace them with their code. */ function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { - const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); + const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text ?? '', preferredSkinTone, lang); return { text: convertedText, - emojis: emojis.concat(extractEmojis(text)), + emojis: emojis.concat(extractEmojis(text ?? '')), cursorPosition, }; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index 87352981ba9a..94e65a48e46b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -19,7 +19,8 @@ function IncrementRenderCount() { function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { // Eventually Auto focus on e2e tests useEffect(() => { - if ((E2EClient.getCurrentActiveTestConfig()?.reportScreen?.autoFocus ?? false) === false) { + const testConfig = E2EClient.getCurrentActiveTestConfig(); + if (testConfig?.reportScreen && typeof testConfig.reportScreen !== 'string' && (testConfig?.reportScreen.autoFocus ?? false) === false) { return; } From b037ab91daadf1b1dcb610f942f7ab385d4f499e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 13:38:04 +0100 Subject: [PATCH 068/297] fix: typecheck --- .../MoneyRequestConfirmationList.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 74ac5e878395..933d604fd3cb 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -337,7 +337,7 @@ function MoneyRequestConfirmationList({ let text; if (isSplitBill && iouAmount === 0) { text = translate('iou.split'); - } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithoutRoute) { + } else if (!!(receiptPath && isTypeRequest) || isDistanceRequestWithoutRoute) { text = translate('iou.request'); if (iouAmount !== 0) { text = translate('iou.requestAmount', {amount: Number(formattedAmount)}); @@ -439,7 +439,7 @@ function MoneyRequestConfirmationList({ translate, toLocaleDigit, ); - IOU.setMoneyRequestMerchant_temporaryForRefactor(transactionID, distanceMerchant); + IOU.setMoneyRequestMerchant_temporaryForRefactor(transactionID ?? '', distanceMerchant); }, [hasRoute, distance, mileageRate?.unit, mileageRate?.rate, mileageRate?.currency, translate, toLocaleDigit, isDistanceRequest, transactionID]); const selectParticipant = useCallback( @@ -633,7 +633,7 @@ function MoneyRequestConfirmationList({ CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', - reportID, + reportID ?? '', Navigation.getActiveRouteWithoutParams(), ), ) @@ -652,7 +652,7 @@ function MoneyRequestConfirmationList({ return; } if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); return; } Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(iouType, reportID)); @@ -671,7 +671,7 @@ function MoneyRequestConfirmationList({ description={translate('common.description')} onPress={() => { if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.DESCRIPTION)); + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.DESCRIPTION)); return; } Navigation.navigate(ROUTES.MONEY_REQUEST_DESCRIPTION.getRoute(iouType, reportID)); @@ -700,7 +700,7 @@ function MoneyRequestConfirmationList({ titleStyle={styles.flex1} onPress={() => { if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.DATE)); + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.DATE)); return; } Navigation.navigate(ROUTES.MONEY_REQUEST_DATE.getRoute(iouType, reportID)); @@ -732,7 +732,7 @@ function MoneyRequestConfirmationList({ titleStyle={styles.flex1} onPress={() => { if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.MERCHANT)); + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.MERCHANT)); return; } Navigation.navigate(ROUTES.MONEY_REQUEST_MERCHANT.getRoute(iouType, reportID)); @@ -755,7 +755,7 @@ function MoneyRequestConfirmationList({ numberOfLinesTitle={2} onPress={() => { if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.CATEGORY)); + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.CATEGORY)); return; } Navigation.navigate(ROUTES.MONEY_REQUEST_CATEGORY.getRoute(iouType, reportID)); @@ -775,7 +775,7 @@ function MoneyRequestConfirmationList({ numberOfLinesTitle={2} onPress={() => { if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.TAG)); + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.TAG)); return; } Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(iouType, reportID)); @@ -796,7 +796,7 @@ function MoneyRequestConfirmationList({ titleStyle={styles.flex1} onPress={() => Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), + ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction?.transactionID ?? '', reportID ?? '', Navigation.getActiveRouteWithoutParams()), ) } disabled={didConfirm} @@ -813,7 +813,7 @@ function MoneyRequestConfirmationList({ titleStyle={styles.flex1} onPress={() => Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), + ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction?.transactionID ?? '', reportID ?? '', Navigation.getActiveRouteWithoutParams()), ) } disabled={didConfirm} From eb50d450940d30b3d946f705ad795e695cbf8448 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 30 Jan 2024 20:00:30 +0700 Subject: [PATCH 069/297] add missing mock functions for navigationRef --- tests/perf-test/SignInPage.perf-test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/perf-test/SignInPage.perf-test.tsx b/tests/perf-test/SignInPage.perf-test.tsx index 80964c3c49cd..c7c3eebc25a4 100644 --- a/tests/perf-test/SignInPage.perf-test.tsx +++ b/tests/perf-test/SignInPage.perf-test.tsx @@ -25,6 +25,8 @@ jest.mock('../../src/libs/Navigation/Navigation', () => { navigationRef: { addListener: () => jest.fn(), removeListener: () => jest.fn(), + isReady: () => jest.fn(), + getCurrentRoute: () => jest.fn(), }, } as typeof Navigation; }); From 086a705a923bbffe7e4960b7335d8aff588ce2be Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 2 Feb 2024 10:52:24 +0700 Subject: [PATCH 070/297] resolve conflict --- src/pages/ProfilePage.js | 45 ++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index f20055190e3e..16ace231eceb 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -5,6 +5,7 @@ import React, {useEffect} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import AttachmentModal from '@components/AttachmentModal'; import AutoUpdateTime from '@components/AutoUpdateTime'; import Avatar from '@components/Avatar'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -103,6 +104,8 @@ function ProfilePage(props) { const login = lodashGet(details, 'login', ''); const timezone = lodashGet(details, 'timezone', {}); + const originalFileName = lodashGet(details, 'originalFileName', ''); + // If we have a reportID param this means that we // arrived here via the ParticipantsPage and should be allowed to navigate back to it const shouldShowLocalTime = !ReportUtils.hasAutomatedExpensifyAccountIDs([accountID]) && !_.isEmpty(timezone); @@ -128,7 +131,7 @@ function ProfilePage(props) { const hasStatus = !!statusEmojiCode; const statusContent = `${statusEmojiCode} ${statusText}`; - const navigateBackTo = lodashGet(props.route, 'params.backTo', ROUTES.HOME); + const navigateBackTo = lodashGet(props.route, 'params.backTo'); const shouldShowNotificationPreference = !_.isEmpty(props.report) && props.report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; const notificationPreference = shouldShowNotificationPreference ? props.translate(`notificationPreferencesPage.notificationPreferences.${props.report.notificationPreference}`) : ''; @@ -151,22 +154,32 @@ function ProfilePage(props) { {hasMinimumDetails && ( - Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))} - accessibilityLabel={props.translate('common.profile')} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + - - - - + {({show}) => ( + + + + + + )} + {Boolean(displayName) && ( Date: Fri, 2 Feb 2024 14:35:33 +0100 Subject: [PATCH 071/297] fix: sync with main --- src/components/MoneyRequestConfirmationList.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 933d604fd3cb..30cd28541181 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -660,8 +660,14 @@ function MoneyRequestConfirmationList({ style={[styles.moneyRequestMenuItem, styles.mt2]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} + brickRoadIndicator={ + isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined + } + error={ + shouldDisplayMerchantError || (isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null)) + ? translate('common.error.enterMerchant') + : '' + } /> )} Date: Fri, 2 Feb 2024 15:00:47 +0100 Subject: [PATCH 072/297] fix: resolve comments --- src/components/MoneyRequestConfirmationList.tsx | 7 +++++-- src/libs/ReceiptUtils.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 30cd28541181..685559835897 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -423,7 +423,10 @@ function MoneyRequestConfirmationList({ return []; } const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); - return [...selectedParticipantsMemo, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo, String(myIOUAmount))]; + return [ + ...selectedParticipantsMemo, + OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo, CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode)), + ]; }, [hasMultipleParticipants, selectedParticipantsMemo, iouAmount, iouCurrencyCode, payeePersonalDetailsMemo]); useEffect(() => { @@ -560,7 +563,7 @@ function MoneyRequestConfirmationList({ return ( <> - {formError && ( + {!!formError && ( , receiptPa // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg // If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one - const errors = _.findLast(transaction.errors) as ReceiptError | undefined; + const errors = _.findLast(transaction?.errors) as ReceiptError | undefined; const path = errors?.source ?? transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? ''; From 45c6530265b83384f1b06b8f58725e266aa7d315 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 2 Feb 2024 16:57:35 +0100 Subject: [PATCH 073/297] fix: typecheck --- src/components/AttachmentModal.tsx | 8 ++++---- .../API/parameters/AddCommentOrAttachementParams.ts | 4 +++- src/libs/ReportUtils.ts | 5 +++-- src/libs/actions/Report.ts | 5 +++-- .../AttachmentPickerWithMenuItems.tsx | 3 ++- .../ComposerWithSuggestions.tsx | 7 +++++-- .../ReportActionCompose/ReportActionCompose.tsx | 12 +++++------- .../SilentCommentUpdater/types.ts | 4 ---- src/types/onyx/ReportAction.ts | 3 ++- 9 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 90954c63b751..f4b016b790de 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -80,7 +80,7 @@ type ImagePickerResponse = { type FileObject = File | ImagePickerResponse; type ChildrenProps = { - displayFileInModal: (data: FileObject) => void; + displayFileInModal: (data: FileObject | undefined) => void; show: () => void; }; @@ -317,8 +317,8 @@ function AttachmentModal({ }, []); const validateAndDisplayFileToUpload = useCallback( - (data: FileObject) => { - if (!isDirectoryCheck(data)) { + (data: FileObject | undefined) => { + if (!data || !isDirectoryCheck(data)) { return; } let fileObject = data; @@ -617,4 +617,4 @@ export default withOnyx({ }, })(memo(AttachmentModal)); -export type {Attachment}; +export type {Attachment, FileObject}; diff --git a/src/libs/API/parameters/AddCommentOrAttachementParams.ts b/src/libs/API/parameters/AddCommentOrAttachementParams.ts index 58faf9fdfc9c..4eab35be7dd2 100644 --- a/src/libs/API/parameters/AddCommentOrAttachementParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachementParams.ts @@ -1,9 +1,11 @@ +import type {FileObject} from '@components/AttachmentModal'; + type AddCommentOrAttachementParams = { reportID: string; reportActionID?: string; commentReportActionID?: string | null; reportComment?: string; - file?: File; + file?: Partial; timezone?: string; shouldAllowActionableMentionWhispers?: boolean; clientCreatedTime?: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5f3efcbcdbb0..651cd1f6513d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8,6 +8,7 @@ import lodashIsEqual from 'lodash/isEqual'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import CONST from '@src/CONST'; @@ -1290,7 +1291,7 @@ function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boo /** * Returns true if Concierge is one of the chat participants (1:1 as well as group chats) */ -function chatIncludesConcierge(report: OnyxEntry): boolean { +function chatIncludesConcierge(report: Partial>): boolean { return Boolean(report?.participantAccountIDs?.length && report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CONCIERGE)); } @@ -2591,7 +2592,7 @@ function getParsedComment(text: string): string { return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text); } -function buildOptimisticAddCommentReportAction(text?: string, file?: File, actorAccountID?: number): OptimisticReportAction { +function buildOptimisticAddCommentReportAction(text?: string, file?: Partial, actorAccountID?: number): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); const isAttachment = !text && file !== undefined; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 782cf2b174c2..a508325a3206 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -7,6 +7,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-nat import Onyx from 'react-native-onyx'; import type {PartialDeep, ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; +import type {FileObject} from '@components/AttachmentModal'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; import type { @@ -352,7 +353,7 @@ function notifyNewAction(reportID: string, accountID?: number, reportActionID?: * - Adding one attachment * - Add both a comment and attachment simultaneously */ -function addActions(reportID: string, text = '', file?: File) { +function addActions(reportID: string, text = '', file?: Partial) { let reportCommentText = ''; let reportCommentAction: OptimisticAddCommentReportAction | undefined; let attachmentAction: OptimisticAddCommentReportAction | undefined; @@ -511,7 +512,7 @@ function addActions(reportID: string, text = '', file?: File) { } /** Add an attachment and optional comment. */ -function addAttachment(reportID: string, file: File, text = '') { +function addAttachment(reportID: string, file: Partial, text = '') { addActions(reportID, text, file); } diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 0dbfbba2759d..9511498ea699 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -6,6 +6,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -46,7 +47,7 @@ type AttachmentPickerWithMenuItemsProps = { report: OnyxEntry; /** Callback to open the file in the modal */ - displayFileInModal: (url: string) => void; + displayFileInModal: (url: FileObject | undefined) => void; /** Whether or not the full size composer is available */ isFullComposerAvailable: boolean; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 074383897dac..76388372dd27 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -16,6 +16,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {useAnimatedRef} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; +import type {FileObject} from '@components/AttachmentModal'; import Composer from '@components/Composer'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; @@ -85,7 +86,7 @@ type ComposerWithSuggestionsProps = { isComposerFullSize: boolean; isMenuVisible: boolean; inputPlaceholder: string; - displayFileInModal: (file: File | undefined) => void; + displayFileInModal: (file: FileObject | undefined) => void; textInputShouldClear: boolean; setTextInputShouldClear: (shouldClear: boolean) => void; isBlockedFromConcierge: boolean; @@ -107,6 +108,8 @@ type ComposerWithSuggestionsProps = { lastReportAction?: OnyxTypes.ReportAction; includeChronos?: boolean; parentReportActionID?: string; + // eslint-disable-next-line react/no-unused-prop-types + parentReportID: string | undefined; } & ComposerWithSuggestionsOnyxProps & Partial; @@ -748,7 +751,7 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, + key: ({parentReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, canEvict: false, initWithStoredValues: false, }, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index a5646f9c99a3..02d4105c3500 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -76,9 +76,6 @@ type ReportActionComposeProps = { /** The ID of the report actions will be created for */ reportID: string; - /** Array of report actions for this report */ - reportActions?: OnyxTypes.ReportAction[]; - /** The report currently being looked at */ report: OnyxEntry; @@ -97,9 +94,11 @@ type ReportActionComposeProps = { /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; + /** Whether the chat is empty */ isEmptyChat?: boolean; - lastReportAction?: any; + /** The last report action */ + lastReportAction?: OnyxTypes.ReportAction; } & ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps; @@ -118,7 +117,6 @@ function ReportActionCompose({ pendingAction, report, reportID, - reportActions, listHeight = 0, shouldShowComposeInput = true, isReportReadyForDisplay = true, @@ -269,7 +267,7 @@ function ReportActionCompose({ }, []); const addAttachment = useCallback( - (file: FileObject) => { + (file: Partial) => { const newComment = composerRef.current?.prepareCommentAndResetComposer(); Report.addAttachment(reportID, file, newComment); setTextInputShouldClear(false); @@ -437,7 +435,7 @@ function ReportActionCompose({ reportID={reportID} parentReportID={report?.parentReportID} parentReportActionID={report?.parentReportActionID} - includesChronos={ReportUtils.chatIncludesChronos(report)} + includeChronos={ReportUtils.chatIncludesChronos(report)} isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} isMenuVisible={isMenuVisible} diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts index 591ee43ce6cd..1ba1689e7d6d 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -1,5 +1,4 @@ import type {OnyxEntry} from 'react-native-onyx'; -import type {Report} from '@src/types/onyx'; type SilentCommentUpdaterOnyxProps = { /** The comment of the report */ @@ -13,9 +12,6 @@ type SilentCommentUpdaterProps = { /** The ID of the report associated with the comment */ reportID: string; - /** The report associated with the comment */ - report: OnyxEntry; - /** The value of the comment */ value: string; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index d2f0afad5b7a..59fb785234eb 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,4 +1,5 @@ import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import type {AvatarSource} from '@libs/UserUtils'; import type CONST from '@src/CONST'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; @@ -165,7 +166,7 @@ type ReportActionBase = { isFirstItem?: boolean; /** Informations about attachments of report action */ - attachmentInfo?: File | EmptyObject; + attachmentInfo?: Partial | EmptyObject; /** Receipt tied to report action */ receipt?: Receipt; From 61ad4769e0215b48706a74615e843fb765eeab58 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 2 Feb 2024 18:51:38 +0100 Subject: [PATCH 074/297] fix: resolve comments --- src/components/Composer/types.ts | 4 +- src/components/LocaleContextProvider.tsx | 4 +- src/libs/ComposerUtils/index.ts | 2 +- src/libs/DateUtils.ts | 2 +- src/libs/E2E/tests/chatOpeningTest.e2e.ts | 5 +- src/libs/E2E/tests/reportTypingTest.e2e.ts | 5 +- src/libs/EmojiUtils.ts | 7 +- .../home/report/ParticipantLocalTime.tsx | 4 +- .../AttachmentPickerWithMenuItems.tsx | 2 +- .../ComposerWithSuggestions.tsx | 73 +++++++++++++++++-- .../ComposerWithSuggestions/index.e2e.tsx | 8 +- .../ReportActionCompose.tsx | 46 ++++++------ .../report/ReportActionCompose/SendButton.tsx | 2 - .../SilentCommentUpdater/index.android.tsx | 2 +- .../SilentCommentUpdater/index.tsx | 4 +- .../SilentCommentUpdater/types.ts | 6 +- .../ReportActionCompose/SuggestionEmoji.tsx | 13 ++-- .../ReportActionCompose/SuggestionMention.tsx | 10 +-- .../ReportActionCompose/Suggestions.tsx | 21 ++++++ .../home/report/ReportTypingIndicator.tsx | 4 +- 20 files changed, 147 insertions(+), 77 deletions(-) diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 9565eaf6208f..f19feb94dd0a 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -5,7 +5,7 @@ type TextSelection = { end?: number; }; -type ComposerProps = { +type ComposerProps = TextInputProps & { /** identify id in the text input */ id?: string; @@ -76,6 +76,6 @@ type ComposerProps = { /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; -} & TextInputProps; +}; export type {TextSelection, ComposerProps}; diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 25b468181b87..6d819f4d6eaa 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -45,7 +45,7 @@ type LocaleContextProps = { /** Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions */ - formatPhoneNumber: (phoneNumber: string) => string; + formatPhoneNumber: (phoneNumber: string | undefined) => string; /** Gets the locale digit corresponding to a standard digit */ toLocaleDigit: (digit: string) => string; @@ -94,7 +94,7 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {} const updateLocale = useMemo(() => () => DateUtils.setLocale(locale), [locale]); - const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber), []); + const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber ?? ''), []); const toLocaleDigit = useMemo(() => (digit) => LocaleDigitUtils.toLocaleDigit(locale, digit), [locale]); diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index de937c0ba915..6018cad86e47 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -18,7 +18,7 @@ function insertText(text: string, selection: Selection, textToInsert: string): s * Insert a white space at given index of text * @param text - text that needs whitespace to be appended to */ -function insertWhiteSpaceAtIndex(text: string | null, index: number) { +function insertWhiteSpaceAtIndex(text: string, index: number) { return `${text?.slice(0, index)} ${text?.slice(index)}`; } diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 9cb08556f082..c10d6b90128b 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -265,7 +265,7 @@ function formatToLongDateWithWeekday(datetime: string | Date): string { * * @returns Sunday */ -function formatToDayOfWeek(datetime: string): string { +function formatToDayOfWeek(datetime: Date): string { return format(new Date(datetime), CONST.DATE.WEEKDAY_TIME_FORMAT); } diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index 2e2ce76b348d..17d9dfa1cb4d 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -2,18 +2,17 @@ import type {NativeConfig} from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; -import type {TestConfig} from '@libs/E2E/types'; import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -const test = (config: TestConfig) => { +const test = (config: NativeConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); + const reportID = getConfigValueOrThrow('reportID', config); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index 17464f7eb8d6..817bda941611 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -4,7 +4,6 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; import E2EClient from '@libs/E2E/client'; -import type {TestConfig} from '@libs/E2E/types'; import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; @@ -13,11 +12,11 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import * as NativeCommands from '../../../../tests/e2e/nativeCommands/NativeCommandsAction'; -const test = (config: TestConfig) => { +const test = (config: NativeConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for typing'); - const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); + const reportID = getConfigValueOrThrow('reportID', config); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index a7063e204efe..819aecf09d43 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -10,7 +10,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx'; import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportActionReactions'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {SupportedLanguage} from './EmojiTrie'; type HeaderIndice = {code: string; index: number; icon: IconAsset}; type EmojiSpacer = {code: string; spacer: boolean}; @@ -370,12 +369,12 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF /** * Find all emojis in a text and replace them with their code. */ -function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text ?? '', preferredSkinTone, lang); return { text: convertedText, - emojis: emojis.concat(extractEmojis(text ?? '')), + emojis: emojis.concat(extractEmojis(text)), cursorPosition, }; } @@ -384,7 +383,7 @@ function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number * Suggest emojis when typing emojis prefix after colon * @param [limit] - matching emojis limit */ -function suggestEmojis(text: string, lang: SupportedLanguage, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { +function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index e97fad8f20f0..58d85c0662b2 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -22,8 +22,8 @@ function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); - const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone.toDateString()); - const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone.toDateString()); + const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone); + const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone); if (reportRecipientDay !== currentUserDay) { return `${DateUtils.formatToLocalTime(reportTimezone)} ${reportRecipientDay}`; } diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 9511498ea699..e6a75e6f157b 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -194,7 +194,7 @@ function AttachmentPickerWithMenuItems({ return ( - {/* @ts-expect-error TODO: Remove this once SettlementButton (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} + {/* @ts-expect-error TODO: Remove this once AttachmentPicker (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} {({openPicker}) => { const triggerAttachmentPicker = () => { onTriggerAttachmentPicker(); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 76388372dd27..96989b2a647b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,6 +1,6 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; -import type {ForwardedRef, RefAttributes, RefObject} from 'react'; +import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react'; import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type { LayoutChangeEvent, @@ -79,35 +79,94 @@ type ComposerWithSuggestionsOnyxProps = { }; type ComposerWithSuggestionsProps = { + /** Report ID */ reportID: string; + + /** Callback to focus composer */ onFocus: () => void; + + /** Callback to blur composer */ onBlur: (event: NativeSyntheticEvent) => void; + + /** Callback to update the value of the composer */ onValueChange: (value: string) => void; + + /** Whether the composer is full size */ isComposerFullSize: boolean; + + /** Whether the menu is visible */ isMenuVisible: boolean; + + /** The placeholder for the input */ inputPlaceholder: string; + + /** Function to display a file in a modal */ displayFileInModal: (file: FileObject | undefined) => void; + + /** Whether the text input should clear */ textInputShouldClear: boolean; + + /** Function to set the text input should clear */ setTextInputShouldClear: (shouldClear: boolean) => void; + + /** Whether the user is blocked from concierge */ isBlockedFromConcierge: boolean; + + /** Whether the input is disabled */ disabled: boolean; + + /** Whether the full composer is available */ isFullComposerAvailable: boolean; + + /** Function to set whether the full composer is available */ setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + + /** Function to set whether the comment is empty */ setIsCommentEmpty: (isCommentEmpty: boolean) => void; + + /** Function to handle sending a message */ handleSendMessage: () => void; + + /** Whether the compose input should show */ shouldShowComposeInput: OnyxEntry; + + /** Function to measure the parent container */ measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + + /** The height of the list */ listHeight: number; + + /** Whether the scroll is likely to trigger a layout */ isScrollLikelyLayoutTriggered: RefObject; + + /** Function to raise the scroll is likely layout triggered */ raiseIsScrollLikelyLayoutTriggered: () => void; + + /** The ref to the suggestions */ suggestionsRef: React.RefObject; + + /** The ref to the animated input */ animatedRef: AnimatedRef; - isNextModalWillOpenRef: RefObject; + + /** The ref to the next modal will open */ + isNextModalWillOpenRef: MutableRefObject; + + /** Whether the edit is focused */ editFocused: boolean; + + /** Wheater chat is empty */ isEmptyChat?: boolean; + + /** The last report action */ lastReportAction?: OnyxTypes.ReportAction; + + /** Whether to include chronos */ includeChronos?: boolean; + + /** The parent report action ID */ parentReportActionID?: string; + + /** The parent report ID */ // eslint-disable-next-line react/no-unused-prop-types parentReportID: string | undefined; } & ComposerWithSuggestionsOnyxProps & @@ -275,13 +334,13 @@ function ComposerWithSuggestions( * @property diff - The newly added characters. */ const findNewlyAddedChars = useCallback( - (prevText: string, newText: string | null): NewlyAddedChars => { + (prevText: string, newText: string): NewlyAddedChars => { let startIndex = -1; let endIndex = -1; let currentIndex = 0; // Find the first character mismatch with newText - while (currentIndex < (newText?.length ?? 0) && prevText.charAt(currentIndex) === newText?.charAt(currentIndex) && selection.start > currentIndex) { + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText?.charAt(currentIndex) && selection.start > currentIndex) { currentIndex++; } @@ -295,7 +354,6 @@ function ComposerWithSuggestions( endIndex = currentIndex + (newText?.length ?? 0); } } - return { startIndex, endIndex, @@ -309,7 +367,7 @@ function ComposerWithSuggestions( * Update the value of the comment in Onyx */ const updateComment = useCallback( - (commentValue: string | null, shouldDebounceSaveComment?: boolean) => { + (commentValue: string, shouldDebounceSaveComment?: boolean) => { raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); @@ -317,7 +375,7 @@ function ComposerWithSuggestions( text: newComment, emojis, cursorPosition, - } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); + } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue ?? '', preferredSkinTone, preferredLocale); if (emojis.length) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis.length) { @@ -595,7 +653,6 @@ function ComposerWithSuggestions( const prevIsFocused = usePrevious(isFocused); useEffect(() => { if (modal?.isVisible && !prevIsModalVisible) { - // @ts-expect-error need to reassign this ref // eslint-disable-next-line no-param-reassign isNextModalWillOpenRef.current = false; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index 94e65a48e46b..0fa535329d33 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -1,4 +1,4 @@ -import type {ForwardedRef, RefObject} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect} from 'react'; import E2EClient from '@libs/E2E/client'; import type {ComposerRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; @@ -20,17 +20,17 @@ function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: Fo // Eventually Auto focus on e2e tests useEffect(() => { const testConfig = E2EClient.getCurrentActiveTestConfig(); - if (testConfig?.reportScreen && typeof testConfig.reportScreen !== 'string' && (testConfig?.reportScreen.autoFocus ?? false) === false) { + if (testConfig?.reportScreen && typeof testConfig.reportScreen !== 'string' && !testConfig?.reportScreen.autoFocus) { return; } // We need to wait for the component to be mounted before focusing setTimeout(() => { - if (!(ref as RefObject)?.current) { + if (!(ref && 'current' in ref)) { return; } - (ref as RefObject).current?.focus(true); + ref.current?.focus(true); }, 1); }, [ref]); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 02d4105c3500..98630ff88b23 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -69,38 +69,38 @@ type ReportActionComposeOnyxProps = { shouldShowComposeInput: OnyxEntry; }; -type ReportActionComposeProps = { - /** A method to call when the form is submitted */ - onSubmit: (newComment: string | undefined) => void; +type ReportActionComposeProps = ReportActionComposeOnyxProps & + WithCurrentUserPersonalDetailsProps & { + /** A method to call when the form is submitted */ + onSubmit: (newComment: string | undefined) => void; - /** The ID of the report actions will be created for */ - reportID: string; + /** The ID of the report actions will be created for */ + reportID: string; - /** The report currently being looked at */ - report: OnyxEntry; + /** The report currently being looked at */ + report: OnyxEntry; - /** Is composer full size */ - isComposerFullSize?: boolean; + /** Is composer full size */ + isComposerFullSize?: boolean; - /** Whether user interactions should be disabled */ - disabled?: boolean; + /** Whether user interactions should be disabled */ + disabled?: boolean; - /** Height of the list which the composer is part of */ - listHeight?: number; + /** Height of the list which the composer is part of */ + listHeight?: number; - /** The type of action that's pending */ - pendingAction?: OnyxCommon.PendingAction; + /** The type of action that's pending */ + pendingAction?: OnyxCommon.PendingAction; - /** Whether the report is ready for display */ - isReportReadyForDisplay?: boolean; + /** Whether the report is ready for display */ + isReportReadyForDisplay?: boolean; - /** Whether the chat is empty */ - isEmptyChat?: boolean; + /** Whether the chat is empty */ + isEmptyChat?: boolean; - /** The last report action */ - lastReportAction?: OnyxTypes.ReportAction; -} & ReportActionComposeOnyxProps & - WithCurrentUserPersonalDetailsProps; + /** The last report action */ + lastReportAction?: OnyxTypes.ReportAction; + }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus on existing chat for mobile device diff --git a/src/pages/home/report/ReportActionCompose/SendButton.tsx b/src/pages/home/report/ReportActionCompose/SendButton.tsx index feeff49ceb55..4726c1638f42 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.tsx +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -46,8 +46,6 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP ]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.send')} - accessible - onPress={() => {}} > {({pressed}) => ( { - updateComment(comment); + updateComment(comment ?? ''); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index a1d591c97297..c84bd3786610 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -17,7 +17,7 @@ function SilentCommentUpdater({comment, commentRef, reportID, value, updateComme const prevPreferredLocale = usePrevious(preferredLocale); useEffect(() => { - updateComment(comment ?? null); + updateComment(comment ?? ''); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); @@ -32,7 +32,7 @@ function SilentCommentUpdater({comment, commentRef, reportID, value, updateComme return; } - updateComment(comment); + updateComment(comment ?? ''); }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef]); return null; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts index 1ba1689e7d6d..dbc23b0279c3 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -5,9 +5,9 @@ type SilentCommentUpdaterOnyxProps = { comment: OnyxEntry; }; -type SilentCommentUpdaterProps = { +type SilentCommentUpdaterProps = SilentCommentUpdaterOnyxProps & { /** Updates the comment */ - updateComment: (comment: OnyxEntry) => void; + updateComment: (comment: string) => void; /** The ID of the report associated with the comment */ reportID: string; @@ -17,6 +17,6 @@ type SilentCommentUpdaterProps = { /** The ref of the comment */ commentRef: React.RefObject; -} & SilentCommentUpdaterOnyxProps; +}; export type {SilentCommentUpdaterProps, SilentCommentUpdaterOnyxProps}; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index d343616a206d..206c91ed9b08 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -6,7 +6,6 @@ import type {Emoji} from '@assets/emojis/types'; import EmojiSuggestions from '@components/EmojiSuggestions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; -import type {SupportedLanguage} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; @@ -26,11 +25,11 @@ type SuggestionEmojiOnyxProps = { preferredSkinTone: number; }; -type SuggestionEmojiProps = { - /** Function to clear the input */ - resetKeyboardInput: (() => void) | undefined; -} & SuggestionEmojiOnyxProps & - SuggestionProps; +type SuggestionEmojiProps = SuggestionProps & + SuggestionEmojiOnyxProps & { + /** Function to clear the input */ + resetKeyboardInput: (() => void) | undefined; + }; /** * Check if this piece of string looks like an emoji @@ -166,7 +165,7 @@ function SuggestionEmoji( colonIndex, shouldShowSuggestionMenu: false, }; - const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale as SupportedLanguage); + const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale); if (newSuggestedEmojis?.length && isCurrentlyShowingEmojiSuggestion) { nextState.suggestedEmojis = newSuggestedEmojis; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 02bcd27093e7..1e3e80ad9d7e 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -15,8 +15,6 @@ import type {PersonalDetailsList} from '@src/types/onyx'; import type {SuggestionsRef} from './ReportActionCompose'; import type {SuggestionProps} from './Suggestions'; -type SuggestionMentionProps = {isAutoSuggestionPickerLarge: boolean} & SuggestionProps; - type SuggestionValues = { suggestedMentions: Mention[]; atSignIndex: number; @@ -37,7 +35,7 @@ const defaultSuggestionsValues: SuggestionValues = { }; function SuggestionMention( - {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused}: SuggestionMentionProps, + {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused}: SuggestionProps, ref: ForwardedRef, ) { const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; @@ -161,7 +159,7 @@ function SuggestionMention( sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), - alternateText: formatPhoneNumber(detail?.login ?? ''), + alternateText: formatPhoneNumber(detail?.login), login: detail?.login, icons: [ { @@ -200,7 +198,7 @@ function SuggestionMention( const leftString = value.substring(0, suggestionEndIndex); const words = leftString.split(CONST.REGEX.SPACE_OR_EMOJI); - const lastWord = words.at(-1) ?? ''; + const lastWord: string = words.at(-1) ?? ''; const secondToLastWord = words[words.length - 3]; let atSignIndex; @@ -297,7 +295,7 @@ function SuggestionMention( mentions={suggestionValues.suggestedMentions} prefix={suggestionValues.mentionPrefix} onSelect={insertSelectedMention} - isMentionPickerLarge={isAutoSuggestionPickerLarge} + isMentionPickerLarge={!!isAutoSuggestionPickerLarge} measureParentContainer={measureParentContainer} /> ); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 0968a61c3abb..61026a792919 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -14,16 +14,37 @@ type Selection = { }; type SuggestionProps = { + /** The current input value */ value: string; + + /** Callback to update the current input value */ setValue: (newValue: string) => void; + + /** The current selection value */ selection: Selection; + + /** Callback to update the current selection */ setSelection: (newSelection: Selection) => void; + + /** Callback to update the comment draft */ updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; + + /** Meaures the parent container's position and dimensions. */ measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + + /** Whether the composer is expanded */ isComposerFullSize: boolean; + + /** Report composer focus state */ isComposerFocused?: boolean; + + /** Callback to reset the keyboard input */ resetKeyboardInput?: () => void; + + /** Whether the auto suggestion picker is large */ isAutoSuggestionPickerLarge?: boolean; + + /** The height of the composer */ composerHeight?: number; }; diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index 6b00a4520295..e484ce9886a7 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -15,10 +15,10 @@ type ReportTypingIndicatorOnyxProps = { userTypingStatuses: OnyxEntry; }; -type ReportTypingIndicatorProps = { +type ReportTypingIndicatorProps = ReportTypingIndicatorOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types -- This is used by withOnyx reportID: string; -} & ReportTypingIndicatorOnyxProps; +}; function ReportTypingIndicator({userTypingStatuses}: ReportTypingIndicatorProps) { const {translate} = useLocalize(); From 3f10fe467b1bd17408eb8ffa1a8bb628a2429e46 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 5 Feb 2024 17:48:58 +0700 Subject: [PATCH 075/297] fix update amount --- src/libs/actions/IOU.js | 73 ++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f5167eed9afa..bd9773dcbfa2 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -312,15 +312,33 @@ function getReceiptError(receipt, filename, isScanRequest = true) { : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source, filename}); } +/** + * @param {Object} iouReport + * + * @returns {Boolean} + */ +function needsToBeManuallySubmitted(iouReport) { + const isPolicyExpenseChat = ReportUtils.isExpenseReport(iouReport); + + if (isPolicyExpenseChat) { + const policy = ReportUtils.getPolicy(iouReport.policyID); + const isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); + + // If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN + return isFromPaidPolicy && !(policy.isHarvestingEnabled || false); + } + + return true; +} + /** * Return the object to update hasOutstandingChildRequest * @param {Object} [policy] * @param {Object} iouReport - * @param {Boolean} needsToBeManuallySubmitted * @returns {Object} */ -function getOutstandingChildRequest(policy, iouReport, needsToBeManuallySubmitted) { - if (!needsToBeManuallySubmitted) { +function getOutstandingChildRequest(policy, iouReport) { + if (!needsToBeManuallySubmitted(iouReport)) { return { hasOutstandingChildRequest: false, }; @@ -355,7 +373,6 @@ function getOutstandingChildRequest(policy, iouReport, needsToBeManuallySubmitte * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) * @param {Array} policyTags * @param {Array} policyCategories - * @param {Boolean} needsToBeManuallySubmitted * @returns {Array} - An array containing the optimistic data, success data, and failure data. */ function buildOnyxDataForMoneyRequest( @@ -374,10 +391,9 @@ function buildOnyxDataForMoneyRequest( policy, policyTags, policyCategories, - needsToBeManuallySubmitted = true, ) { const isScanRequest = TransactionUtils.isScanRequest(transaction); - const outstandingChildRequest = getOutstandingChildRequest(policy, iouReport, needsToBeManuallySubmitted); + const outstandingChildRequest = getOutstandingChildRequest(policy, iouReport); const optimisticData = [ { // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page @@ -722,15 +738,10 @@ function getMoneyRequestInformation( iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; } - // Check if the Scheduled Submit is enabled in case of expense report - let needsToBeManuallySubmitted = true; let isFromPaidPolicy = false; if (isPolicyExpenseChat) { isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); - // If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN - needsToBeManuallySubmitted = isFromPaidPolicy && !(lodashGet(policy, 'harvesting.enabled', policy.isHarvestingEnabled) || false); - // If the linked expense report on paid policy is not draft, we need to create a new draft expense report if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) { iouReport = null; @@ -859,7 +870,6 @@ function getMoneyRequestInformation( policy, policyTags, policyCategories, - needsToBeManuallySubmitted, ); return { @@ -1033,7 +1043,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t // from the server with the currency conversion let updatedMoneyRequestReport = {...iouReport}; if (updatedTransaction.currency === iouReport.currency && updatedTransaction.modifiedAmount) { - const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); + const diff = TransactionUtils.getAmount(updatedTransaction, isFromExpenseReport) - TransactionUtils.getAmount(transaction, isFromExpenseReport); if (ReportUtils.isExpenseReport(iouReport)) { updatedMoneyRequestReport.total += diff; } else { @@ -1041,11 +1051,23 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedTransaction.currency); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: updatedMoneyRequestReport, - }); + optimisticData.push( + ...[ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: updatedMoneyRequestReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.parentReportID}`, + value: { + hasOutstandingChildRequest: + needsToBeManuallySubmitted(iouReport) && updatedMoneyRequestReport.managerID === userAccountID && updatedMoneyRequestReport.total !== 0, + }, + }, + ], + ); successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -2653,18 +2675,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const updatedReportPreviewAction = {...reportPreviewAction}; updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; - const isPolicyExpenseChat = ReportUtils.isExpenseReport(iouReport); - - let needsToBeManuallySubmitted = true; - if (isPolicyExpenseChat) { - const policy = ReportUtils.getPolicy(iouReport.policyID); - const isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); - - // If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN - needsToBeManuallySubmitted = isFromPaidPolicy && !(policy.isHarvestingEnabled || false); - } - - if (isPolicyExpenseChat) { + if (ReportUtils.isExpenseReport(iouReport)) { updatedIOUReport = {...iouReport}; // Because of the Expense reports are stored as negative values, we add the total from the amount @@ -2770,7 +2781,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, value: { - hasOutstandingChildRequest: needsToBeManuallySubmitted && updatedIOUReport.managerID === userAccountID && updatedIOUReport.total !== 0, + hasOutstandingChildRequest: needsToBeManuallySubmitted(iouReport) && updatedIOUReport.managerID === userAccountID && updatedIOUReport.total !== 0, }, }, ]; From fad94907fbdbd62dc75a4668055132d0dec9825b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 5 Feb 2024 12:02:47 +0100 Subject: [PATCH 076/297] fix: resolve comments --- .../ReportActionCompose/SuggestionMention.tsx | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 1e3e80ad9d7e..2b5080e788ab 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -1,3 +1,4 @@ +import lodashSortBy from 'lodash/sortBy'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -142,20 +143,8 @@ function SuggestionMention( return true; }); - const sortedPersonalDetails = filteredPersonalDetails.sort((a, b) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null - const nameA = a?.displayName || a?.login || ''; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null - const nameB = b?.displayName || b?.login || ''; - - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - return 0; - }); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing cannot be used if left side can be empty string + const sortedPersonalDetails = lodashSortBy(filteredPersonalDetails, (detail) => detail?.displayName || detail?.login); sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), @@ -201,9 +190,9 @@ function SuggestionMention( const lastWord: string = words.at(-1) ?? ''; const secondToLastWord = words[words.length - 3]; - let atSignIndex; + let atSignIndex: number | undefined; let suggestionWord = ''; - let prefix; + let prefix: string; // Detect if the last two words contain a mention (two words are needed to detect a mention with a space in it) if (lastWord.startsWith('@')) { From 937946954bcce86a8af2b7b9eeca12336e155635 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 5 Feb 2024 22:52:12 +0700 Subject: [PATCH 077/297] seperate platform logic --- .../index.native.ts} | 0 src/libs/actions/CachedPDFPaths/index.ts | 9 +++++++++ 2 files changed, 9 insertions(+) rename src/libs/actions/{CachedPDFPaths.ts => CachedPDFPaths/index.native.ts} (100%) create mode 100644 src/libs/actions/CachedPDFPaths/index.ts diff --git a/src/libs/actions/CachedPDFPaths.ts b/src/libs/actions/CachedPDFPaths/index.native.ts similarity index 100% rename from src/libs/actions/CachedPDFPaths.ts rename to src/libs/actions/CachedPDFPaths/index.native.ts diff --git a/src/libs/actions/CachedPDFPaths/index.ts b/src/libs/actions/CachedPDFPaths/index.ts new file mode 100644 index 000000000000..8c5c71df7a49 --- /dev/null +++ b/src/libs/actions/CachedPDFPaths/index.ts @@ -0,0 +1,9 @@ +function add(reportActionID: string, path: string): Promise { + return Promise.resolve(); +} + +function clearByKey(reportActionID: string) {} + +function clearAll() {} + +export {add, clearByKey, clearAll}; From 6eb146a455a902b3c7fcad8fdd2c93362150a107 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 5 Feb 2024 23:07:20 +0700 Subject: [PATCH 078/297] add types for CachedPDFPaths --- .../Attachments/AttachmentView/index.js | 2 +- src/libs/actions/CachedPDFPaths/index.native.ts | 17 +++++++++-------- src/libs/actions/CachedPDFPaths/index.ts | 10 +++++----- src/libs/actions/CachedPDFPaths/types.ts | 6 ++++++ 4 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 src/libs/actions/CachedPDFPaths/types.ts diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 39f83820d207..cf8cc6f017d9 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -143,7 +143,7 @@ function AttachmentView({ const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; const onPDFLoadComplete = (path) => { - if (isUsedInCarousel && reportActionID) { + if (isUsedInCarousel && reportActionID && path) { CachedPDFPaths.add(reportActionID, path); } if (!loadComplete) { diff --git a/src/libs/actions/CachedPDFPaths/index.native.ts b/src/libs/actions/CachedPDFPaths/index.native.ts index 4eefb9a7982e..6c4456f7a528 100644 --- a/src/libs/actions/CachedPDFPaths/index.native.ts +++ b/src/libs/actions/CachedPDFPaths/index.native.ts @@ -1,6 +1,7 @@ import {exists, unlink} from 'react-native-fs'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Add, Clear, ClearAll, ClearByKey} from './types'; /* * We need to save the paths of PDF files so we can delete them later. @@ -14,14 +15,14 @@ Onyx.connect({ }, }); -function add(reportActionID: string, path: string): Promise { +const add: Add = (reportActionID: string, path: string) => { if (pdfPaths[reportActionID]) { return Promise.resolve(); } return Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {[reportActionID]: path}); -} +}; -function clear(path: string): Promise { +const clear: Clear = (path: string) => { if (!path) { return Promise.resolve(); } @@ -33,14 +34,14 @@ function clear(path: string): Promise { return unlink(path); }); }); -} +}; -function clearByKey(reportActionID: string) { +const clearByKey: ClearByKey = (reportActionID: string) => { clear(pdfPaths[reportActionID] ?? '').then(() => Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {[reportActionID]: null})); -} +}; -function clearAll() { +const clearAll: ClearAll = () => { Promise.all(Object.values(pdfPaths).map(clear)).then(() => Onyx.merge(ONYXKEYS.CACHED_PDF_PATHS, {})); -} +}; export {add, clearByKey, clearAll}; diff --git a/src/libs/actions/CachedPDFPaths/index.ts b/src/libs/actions/CachedPDFPaths/index.ts index 8c5c71df7a49..3cac21bf3c25 100644 --- a/src/libs/actions/CachedPDFPaths/index.ts +++ b/src/libs/actions/CachedPDFPaths/index.ts @@ -1,9 +1,9 @@ -function add(reportActionID: string, path: string): Promise { - return Promise.resolve(); -} +import type {Add, ClearAll, ClearByKey} from './types'; -function clearByKey(reportActionID: string) {} +const add: Add = () => Promise.resolve(); -function clearAll() {} +const clearByKey: ClearByKey = () => {}; + +const clearAll: ClearAll = () => {}; export {add, clearByKey, clearAll}; diff --git a/src/libs/actions/CachedPDFPaths/types.ts b/src/libs/actions/CachedPDFPaths/types.ts new file mode 100644 index 000000000000..8c4843aaeaec --- /dev/null +++ b/src/libs/actions/CachedPDFPaths/types.ts @@ -0,0 +1,6 @@ +type Add = (reportActionID: string, path: string) => Promise; +type Clear = (path: string) => Promise; +type ClearAll = () => void; +type ClearByKey = (reportActionID: string) => void; + +export type {Add, Clear, ClearAll, ClearByKey}; From caa025bb01c10a9a0ef592ff018f4266f024de22 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 6 Feb 2024 11:49:24 +0700 Subject: [PATCH 079/297] fix error message disappear --- src/components/ReportActionItem/MoneyRequestView.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 3a3aef6cabcd..1ca07db2eff5 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -189,10 +189,15 @@ function MoneyRequestView({ const hasReceipt = TransactionUtils.hasReceipt(transaction); let receiptURIs; - let hasErrors = false; + let hasErrors = Boolean( + canEdit && + transaction && + !TransactionUtils.isDistanceRequest(transaction) && + !TransactionUtils.isReceiptBeingScanned(transaction) && + TransactionUtils.areRequiredFieldsEmpty(transaction), + ); if (hasReceipt) { receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction); - hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); } const pendingAction = transaction?.pendingAction; From 1ca73747a73be8446150e43d5c7eed9c5c6cea85 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 6 Feb 2024 13:32:58 +0700 Subject: [PATCH 080/297] fix lint --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 1ca07db2eff5..ae7144e9c6a4 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -189,7 +189,7 @@ function MoneyRequestView({ const hasReceipt = TransactionUtils.hasReceipt(transaction); let receiptURIs; - let hasErrors = Boolean( + const hasErrors = Boolean( canEdit && transaction && !TransactionUtils.isDistanceRequest(transaction) && From 4d0c1b1704d30f2eee6bd85bbc8e4ddca4433e67 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 6 Feb 2024 16:26:36 +0700 Subject: [PATCH 081/297] resolve conflict --- src/pages/ProfilePage.js | 43 ++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 16ace231eceb..426232b2f68d 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -5,7 +5,6 @@ import React, {useEffect} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import AttachmentModal from '@components/AttachmentModal'; import AutoUpdateTime from '@components/AutoUpdateTime'; import Avatar from '@components/Avatar'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -104,8 +103,6 @@ function ProfilePage(props) { const login = lodashGet(details, 'login', ''); const timezone = lodashGet(details, 'timezone', {}); - const originalFileName = lodashGet(details, 'originalFileName', ''); - // If we have a reportID param this means that we // arrived here via the ParticipantsPage and should be allowed to navigate back to it const shouldShowLocalTime = !ReportUtils.hasAutomatedExpensifyAccountIDs([accountID]) && !_.isEmpty(timezone); @@ -154,32 +151,22 @@ function ProfilePage(props) { {hasMinimumDetails && ( - Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))} + accessibilityLabel={props.translate('common.profile')} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} > - {({show}) => ( - - - - - - )} - + + + + {Boolean(displayName) && ( Date: Tue, 6 Feb 2024 14:34:37 +0100 Subject: [PATCH 082/297] fix: typecheck --- src/components/ButtonWithDropdownMenu.tsx | 21 +++++++-------- .../MoneyRequestConfirmationList.tsx | 27 ++++++++++++------- .../API/parameters/PayMoneyRequestParams.ts | 5 ++-- src/libs/API/parameters/SendMoneyParams.ts | 5 ++-- src/libs/actions/IOU.ts | 26 +++++++++--------- src/types/onyx/OriginalMessage.ts | 3 +-- 6 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu.tsx b/src/components/ButtonWithDropdownMenu.tsx index 0e41da57b409..8aa3a5f0b9f0 100644 --- a/src/components/ButtonWithDropdownMenu.tsx +++ b/src/components/ButtonWithDropdownMenu.tsx @@ -10,17 +10,14 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PopoverMenu from './PopoverMenu'; -type PaymentType = DeepValueOf; - -type DropdownOption = { - value: PaymentType; +type DropdownOption = { + value: T; text: string; icon?: IconAsset; iconWidth?: number; @@ -28,15 +25,15 @@ type DropdownOption = { iconDescription?: string; }; -type ButtonWithDropdownMenuProps = { +type ButtonWithDropdownMenuProps = { /** Text to display for the menu header */ menuHeaderText?: string; /** Callback to execute when the main button is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: PaymentType) => void; + onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: T) => void; /** Callback to execute when a dropdown option is selected */ - onOptionSelected?: (option: DropdownOption) => void; + onOptionSelected?: (option: DropdownOption) => void; /** Call the onPress function on main button when Enter key is pressed */ pressOnEnter?: boolean; @@ -55,19 +52,19 @@ type ButtonWithDropdownMenuProps = { /** Menu options to display */ /** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */ - options: DropdownOption[]; + options: Array>; /** The anchor alignment of the popover menu */ anchorAlignment?: AnchorAlignment; /* ref for the button */ - buttonRef: RefObject; + buttonRef?: RefObject; /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ enterKeyEventListenerPriority?: number; }; -function ButtonWithDropdownMenu({ +function ButtonWithDropdownMenu({ isLoading = false, isDisabled = false, pressOnEnter = false, @@ -83,7 +80,7 @@ function ButtonWithDropdownMenu({ options, onOptionSelected, enterKeyEventListenerPriority = 0, -}: ButtonWithDropdownMenuProps) { +}: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index d26bdc139f69..1fd94d4a5f4b 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -25,11 +25,12 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; -import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {MileageRate} from '@src/types/onyx/Policy'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import ConfirmedRoute from './ConfirmedRoute'; import FormHelpMessage from './FormHelpMessage'; @@ -44,6 +45,11 @@ import Text from './Text'; import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; +type DropdownOption = { + text: string; + value: DeepValueOf; +}; + type Option = Partial; type CategorySection = { @@ -79,7 +85,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onConfirm?: (selectedParticipants: Participant[]) => void; /** Callback to parent modal to send money */ - onSendMoney?: (paymentMethod: PaymentMethodType) => void; + onSendMoney?: (paymentMethod: IOU.PaymentMethodType) => void; /** Callback to inform a participant is selected */ onSelectParticipant?: (option: Participant) => void; @@ -130,7 +136,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & isReadOnly?: boolean; /** Depending on expense report or personal IOU report, respective bank account route */ - bankAccountRoute?: string; + bankAccountRoute?: Route; /** The policyID of the request */ policyID?: string; @@ -233,7 +239,7 @@ function MoneyRequestConfirmationList({ const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(policyCategories ?? {})); + const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -250,7 +256,7 @@ function MoneyRequestConfirmationList({ const policyTagList = policyTag?.tags ?? {}; const policyTagListName = policyTag?.name ?? translate('common.tag'); // A flag for showing the tags field - const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledOptions(policyTagList)); + const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledOptions(Object.values(policyTagList ?? {}))); // A flag for showing tax fields - tax rate and tax amount const shouldShowTax = isPolicyExpenseChat && policy?.isTaxTrackingEnabled; @@ -333,7 +339,7 @@ function MoneyRequestConfirmationList({ setDidConfirm(false); } - const splitOrRequestOptions = useMemo(() => { + const splitOrRequestOptions: DropdownOption[] = useMemo(() => { let text; if (isSplitBill && iouAmount === 0) { text = translate('iou.split'); @@ -368,7 +374,7 @@ function MoneyRequestConfirmationList({ if (!canModifyParticipantsValue) { formattedParticipantsList = formattedParticipantsList.map((participant) => ({ ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); } @@ -396,7 +402,7 @@ function MoneyRequestConfirmationList({ } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); sections.push({ title: translate('common.to'), @@ -470,7 +476,7 @@ function MoneyRequestConfirmationList({ }; const confirm = useCallback( - (paymentMethod: PaymentMethodType) => { + (paymentMethod: IOU.PaymentMethodType | undefined) => { if (selectedParticipantsMemo.length === 0) { return; } @@ -555,7 +561,7 @@ function MoneyRequestConfirmationList({ pressOnEnter isDisabled={shouldDisableButton} // eslint-disable-next-line @typescript-eslint/naming-convention - onPress={(_, value) => confirm(value as PaymentMethodType)} + onPress={(_, value) => confirm(value as IOU.PaymentMethodType)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} enterKeyEventListenerPriority={1} @@ -796,6 +802,7 @@ function MoneyRequestConfirmationList({ ); return; } + Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(iouType, reportID)); }} style={styles.moneyRequestMenuItem} diff --git a/src/libs/API/parameters/PayMoneyRequestParams.ts b/src/libs/API/parameters/PayMoneyRequestParams.ts index 94f62bcca065..93117467f630 100644 --- a/src/libs/API/parameters/PayMoneyRequestParams.ts +++ b/src/libs/API/parameters/PayMoneyRequestParams.ts @@ -1,11 +1,10 @@ -import type CONST from '@src/CONST'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {PaymentMethodType} from '@libs/actions/IOU'; type PayMoneyRequestParams = { iouReportID: string; chatReportID: string; reportActionID: string; - paymentMethodType: DeepValueOf; + paymentMethodType: PaymentMethodType; }; export default PayMoneyRequestParams; diff --git a/src/libs/API/parameters/SendMoneyParams.ts b/src/libs/API/parameters/SendMoneyParams.ts index c32287d5b4ec..e4faa8da5b8f 100644 --- a/src/libs/API/parameters/SendMoneyParams.ts +++ b/src/libs/API/parameters/SendMoneyParams.ts @@ -1,11 +1,10 @@ -import type CONST from '@src/CONST'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {PaymentMethodType} from '@libs/actions/IOU'; type SendMoneyParams = { iouReportID: string; chatReportID: string; reportActionID: string; - paymentMethodType: DeepValueOf; + paymentMethodType: PaymentMethodType; transactionID: string; newIOUReportDetails: string; createdReportActionID: string; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 569b6b26b728..3444030240d2 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -63,7 +63,7 @@ type MoneyRequestRoute = StackScreenProps; -type PaymentMethodType = DeepValueOf; +type PaymentMethodType = DeepValueOf | typeof CONST.IOU.REPORT_ACTION_TYPE.APPROVE; type OneOnOneIOUReport = OnyxTypes.Report | undefined | null; @@ -3255,14 +3255,14 @@ function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: Report.notifyNewAction(params.chatReportID, managerID); } -function approveMoneyRequest(expenseReport: OnyxTypes.Report) { - const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; +function approveMoneyRequest(expenseReport: OnyxEntry | EmptyObject) { + const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`] ?? null; - const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID); + const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport?.total ?? 0, expenseReport?.currency ?? '', expenseReport?.reportID ?? ''); const optimisticReportActionsData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { ...(optimisticApprovedReportAction as OnyxTypes.ReportAction), @@ -3272,7 +3272,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report) { }; const optimisticIOUReportData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport?.reportID}`, value: { ...expenseReport, lastMessageText: optimisticApprovedReportAction.message?.[0].text, @@ -3286,7 +3286,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report) { const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { pendingAction: null, @@ -3298,9 +3298,9 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report) { const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, value: { - [expenseReport.reportActionID ?? '']: { + [expenseReport?.reportActionID ?? '']: { errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), }, }, @@ -3310,18 +3310,18 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report) { if (currentNextStep) { optimisticData.push({ onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`, value: null, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`, value: currentNextStep, }); } const parameters: ApproveMoneyRequestParams = { - reportID: expenseReport.reportID, + reportID: expenseReport?.reportID ?? '', approvedReportActionID: optimisticApprovedReportAction.reportActionID, }; @@ -3730,3 +3730,5 @@ export { navigateToStartStepIfScanFileCannotBeRead, savePreferredPaymentMethod, }; + +export type {PaymentMethodType}; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index fd28699e7b4c..34ac84989632 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -1,9 +1,8 @@ import type {ValueOf} from 'type-fest'; +import type {PaymentMethodType} from '@libs/actions/IOU'; import type CONST from '@src/CONST'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -type PaymentMethodType = DeepValueOf; - type ActionName = DeepValueOf; type OriginalMessageActionName = | 'ADDCOMMENT' From 1da3d098f240ba878e7d737a7e32597ca04b1ae9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 8 Feb 2024 14:30:53 +0100 Subject: [PATCH 083/297] fix: typecheck --- src/components/MoneyRequestConfirmationList.tsx | 4 ++-- src/components/SettlementButton.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 98d177a19e67..dbfce22030a2 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -85,7 +85,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onConfirm?: (selectedParticipants: Participant[]) => void; /** Callback to parent modal to send money */ - onSendMoney?: (paymentMethod: IOU.PaymentMethodType) => void; + onSendMoney?: (paymentMethod: OnyxTypes.PaymentMethodType) => void; /** Callback to inform a participant is selected */ onSelectParticipant?: (option: Participant) => void; @@ -567,7 +567,7 @@ function MoneyRequestConfirmationList({ pressOnEnter isDisabled={shouldDisableButton} // eslint-disable-next-line @typescript-eslint/naming-convention - onPress={(_, value) => confirm(value as IOU.PaymentMethodType)} + onPress={(_, value) => confirm(value as OnyxTypes.PaymentMethodType)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} enterKeyEventListenerPriority={1} diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 058def7a34ad..4e96684a65ec 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -226,10 +226,10 @@ function SettlementButton({ buttonRef={buttonRef} isDisabled={isDisabled} isLoading={isLoading} - onPress={(event, iouPaymentType) => selectPaymentType(event, iouPaymentType, triggerKYCFlow)} + onPress={(event, iouPaymentType) => selectPaymentType(event, iouPaymentType as PaymentMethodType, triggerKYCFlow)} pressOnEnter={pressOnEnter} options={paymentButtonOptions} - onOptionSelected={(option) => savePreferredPaymentMethod(policyID, option.value)} + onOptionSelected={(option) => savePreferredPaymentMethod(policyID, option.value as PaymentMethodType)} style={style} buttonSize={buttonSize} anchorAlignment={paymentMethodDropdownAnchorAlignment} From 5735f6163034bff9ad5b24adfbda21caaa132b9c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 8 Feb 2024 18:40:53 +0100 Subject: [PATCH 084/297] fix: resolve comments --- src/components/AttachmentModal.tsx | 4 +- src/components/Composer/types.ts | 2 +- src/components/LocaleContextProvider.tsx | 2 +- src/libs/ComposerUtils/index.ts | 2 +- src/libs/EmojiUtils.ts | 2 +- .../home/report/ParticipantLocalTime.tsx | 13 +- .../AttachmentPickerWithMenuItems.tsx | 21 +-- .../ComposerWithSuggestions.tsx | 146 +++++++++--------- .../ComposerWithSuggestions/index.e2e.tsx | 2 +- .../ReportActionCompose.tsx | 4 +- .../report/ReportActionCompose/SendButton.tsx | 9 +- .../SilentCommentUpdater/index.android.tsx | 2 +- .../ReportActionCompose/SuggestionEmoji.tsx | 2 +- .../home/report/ReportTypingIndicator.tsx | 2 +- 14 files changed, 99 insertions(+), 114 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index adf5b7b232a2..f97da9fcd9b7 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -80,7 +80,7 @@ type ImagePickerResponse = { type FileObject = File | ImagePickerResponse; type ChildrenProps = { - displayFileInModal: (data: FileObject | undefined) => void; + displayFileInModal: (data: FileObject) => void; show: () => void; }; @@ -317,7 +317,7 @@ function AttachmentModal({ }, []); const validateAndDisplayFileToUpload = useCallback( - (data: FileObject | undefined) => { + (data: FileObject) => { if (!data || !isDirectoryCheck(data)) { return; } diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index f19feb94dd0a..6bc44aba69cd 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -31,7 +31,7 @@ type ComposerProps = TextInputProps & { onNumberOfLinesChange?: (numberOfLines: number) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file?: File) => void; + onPasteFile?: (file: File) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 6d819f4d6eaa..eca433025f71 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -94,7 +94,7 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {} const updateLocale = useMemo(() => () => DateUtils.setLocale(locale), [locale]); - const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber ?? ''), []); + const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber), []); const toLocaleDigit = useMemo(() => (digit) => LocaleDigitUtils.toLocaleDigit(locale, digit), [locale]); diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 6018cad86e47..94bba5d0d00c 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -19,7 +19,7 @@ function insertText(text: string, selection: Selection, textToInsert: string): s * @param text - text that needs whitespace to be appended to */ function insertWhiteSpaceAtIndex(text: string, index: number) { - return `${text?.slice(0, index)} ${text?.slice(index)}`; + return `${text.slice(0, index)} ${text.slice(index)}`; } /** diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 05a1cb29141b..cab0f48d75fd 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -370,7 +370,7 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF * Find all emojis in a text and replace them with their code. */ function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { - const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text ?? '', preferredSkinTone, lang); + const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); return { text: convertedText, diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index 58d85c0662b2..f51032690a33 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -12,16 +12,13 @@ import type {PersonalDetails} from '@src/types/onyx'; type ParticipantLocalTimeProps = { /** Personal details of the participant */ participant: PersonalDetails; - - /** The user's preferred locale e.g. 'en', 'es-ES' */ - preferredLocale?: Locale; }; -function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale | undefined) { +function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; - const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); - const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); + const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, undefined, reportRecipientTimezone.selected); + const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale); const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone); const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone); if (reportRecipientDay !== currentUserDay) { @@ -30,8 +27,8 @@ function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: return `${DateUtils.formatToLocalTime(reportTimezone)}`; } -function ParticipantLocalTime({participant, preferredLocale}: ParticipantLocalTimeProps) { - const {translate} = useLocalize(); +function ParticipantLocalTime({participant}: ParticipantLocalTimeProps) { + const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const [localTime, setLocalTime] = useState(() => getParticipantLocalTime(participant, preferredLocale)); diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index e6a75e6f157b..1ae8e63bd0f1 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -1,15 +1,14 @@ import {useIsFocused} from '@react-navigation/native'; -import type {FC} from 'react'; import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {SvgProps} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; @@ -29,25 +28,19 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -type MoneyRequestOption = { - icon: FC; - text: string; - onSelected: () => void; -}; - -type MoneyRequestOptions = Record, MoneyRequestOption>; +type MoneyRequestOptions = Record, PopoverMenuItem>; type AttachmentPickerWithMenuItemsOnyxProps = { /** The policy tied to the report */ policy: OnyxEntry; }; -type AttachmentPickerWithMenuItemsProps = { +type AttachmentPickerWithMenuItemsProps = AttachmentPickerWithMenuItemsOnyxProps & { /** The report currently being looked at */ report: OnyxEntry; /** Callback to open the file in the modal */ - displayFileInModal: (url: FileObject | undefined) => void; + displayFileInModal: (url: FileObject) => void; /** Whether or not the full size composer is available */ isFullComposerAvailable: boolean; @@ -59,7 +52,7 @@ type AttachmentPickerWithMenuItemsProps = { isBlockedFromConcierge: boolean; /** Whether or not the attachment picker is disabled */ - disabled: boolean; + disabled?: boolean; /** Sets the menu visibility */ setMenuVisibility: (isVisible: boolean) => void; @@ -93,7 +86,7 @@ type AttachmentPickerWithMenuItemsProps = { /** The personal details of everyone in the report */ reportParticipantIDs?: number[]; -} & AttachmentPickerWithMenuItemsOnyxProps; +}; /** * This includes the popover of options you see when pressing the + button in the composer. @@ -155,7 +148,7 @@ function AttachmentPickerWithMenuItems({ /** * Determines if we can show the task option */ - const taskOption: MoneyRequestOption[] = useMemo(() => { + const taskOption: PopoverMenuItem[] = useMemo(() => { if (!ReportUtils.canCreateTaskInReport(report)) { return []; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index b54ccaf20518..6b89a591055a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -78,99 +78,99 @@ type ComposerWithSuggestionsOnyxProps = { editFocused: OnyxEntry; }; -type ComposerWithSuggestionsProps = { - /** Report ID */ - reportID: string; +type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & + Partial & { + /** Report ID */ + reportID: string; - /** Callback to focus composer */ - onFocus: () => void; + /** Callback to focus composer */ + onFocus: () => void; - /** Callback to blur composer */ - onBlur: (event: NativeSyntheticEvent) => void; + /** Callback to blur composer */ + onBlur: (event: NativeSyntheticEvent) => void; - /** Callback to update the value of the composer */ - onValueChange: (value: string) => void; + /** Callback to update the value of the composer */ + onValueChange: (value: string) => void; - /** Whether the composer is full size */ - isComposerFullSize: boolean; + /** Whether the composer is full size */ + isComposerFullSize: boolean; - /** Whether the menu is visible */ - isMenuVisible: boolean; + /** Whether the menu is visible */ + isMenuVisible: boolean; - /** The placeholder for the input */ - inputPlaceholder: string; + /** The placeholder for the input */ + inputPlaceholder: string; - /** Function to display a file in a modal */ - displayFileInModal: (file: FileObject | undefined) => void; + /** Function to display a file in a modal */ + displayFileInModal: (file: FileObject) => void; - /** Whether the text input should clear */ - textInputShouldClear: boolean; + /** Whether the text input should clear */ + textInputShouldClear: boolean; - /** Function to set the text input should clear */ - setTextInputShouldClear: (shouldClear: boolean) => void; + /** Function to set the text input should clear */ + setTextInputShouldClear: (shouldClear: boolean) => void; - /** Whether the user is blocked from concierge */ - isBlockedFromConcierge: boolean; + /** Whether the user is blocked from concierge */ + isBlockedFromConcierge: boolean; - /** Whether the input is disabled */ - disabled: boolean; + /** Whether the input is disabled */ + disabled: boolean; - /** Whether the full composer is available */ - isFullComposerAvailable: boolean; + /** Whether the full composer is available */ + isFullComposerAvailable: boolean; - /** Function to set whether the full composer is available */ - setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + /** Function to set whether the full composer is available */ + setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; - /** Function to set whether the comment is empty */ - setIsCommentEmpty: (isCommentEmpty: boolean) => void; + /** Function to set whether the comment is empty */ + setIsCommentEmpty: (isCommentEmpty: boolean) => void; - /** Function to handle sending a message */ - handleSendMessage: () => void; + /** Function to handle sending a message */ + handleSendMessage: () => void; - /** Whether the compose input should show */ - shouldShowComposeInput: OnyxEntry; + /** Whether the compose input should show */ + shouldShowComposeInput: OnyxEntry; - /** Function to measure the parent container */ - measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + /** Function to measure the parent container */ + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; - /** The height of the list */ - listHeight: number; + /** The height of the list */ + listHeight: number; - /** Whether the scroll is likely to trigger a layout */ - isScrollLikelyLayoutTriggered: RefObject; + /** Whether the scroll is likely to trigger a layout */ + isScrollLikelyLayoutTriggered: RefObject; - /** Function to raise the scroll is likely layout triggered */ - raiseIsScrollLikelyLayoutTriggered: () => void; + /** Function to raise the scroll is likely layout triggered */ + raiseIsScrollLikelyLayoutTriggered: () => void; - /** The ref to the suggestions */ - suggestionsRef: React.RefObject; + /** The ref to the suggestions */ + suggestionsRef: React.RefObject; - /** The ref to the animated input */ - animatedRef: AnimatedRef; + /** The ref to the animated input */ + animatedRef: AnimatedRef; - /** The ref to the next modal will open */ - isNextModalWillOpenRef: MutableRefObject; + /** The ref to the next modal will open */ + isNextModalWillOpenRef: MutableRefObject; - /** Whether the edit is focused */ - editFocused: boolean; + /** Whether the edit is focused */ + editFocused: boolean; - /** Wheater chat is empty */ - isEmptyChat?: boolean; + /** Wheater chat is empty */ + isEmptyChat?: boolean; - /** The last report action */ - lastReportAction?: OnyxTypes.ReportAction; + /** The last report action */ + lastReportAction?: OnyxTypes.ReportAction; - /** Whether to include chronos */ - includeChronos?: boolean; + /** Whether to include chronos */ + includeChronos?: boolean; - /** The parent report action ID */ - parentReportActionID?: string; + /** The parent report action ID */ + parentReportActionID?: string; - /** The parent report ID */ - // eslint-disable-next-line react/no-unused-prop-types - parentReportID: string | undefined; -} & ComposerWithSuggestionsOnyxProps & - Partial; + /** The parent report ID */ + // eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC + parentReportID: string | undefined; + }; const {RNTextInputReset} = NativeModules; @@ -240,7 +240,7 @@ function ComposerWithSuggestions( // For testing children, }: ComposerWithSuggestionsProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); @@ -340,24 +340,24 @@ function ComposerWithSuggestions( let currentIndex = 0; // Find the first character mismatch with newText - while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText?.charAt(currentIndex) && selection.start > currentIndex) { + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { currentIndex++; } - if (currentIndex < (newText?.length ?? 0)) { + if (currentIndex < newText.length) { startIndex = currentIndex; - const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText ?? '', selection.end); + const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText, selection.end); // if text is getting pasted over find length of common suffix and subtract it from new text length if (commonSuffixLength > 0 || selection.end - selection.start > 0) { - endIndex = (newText?.length ?? 0) - commonSuffixLength; + endIndex = newText.length - commonSuffixLength; } else { - endIndex = currentIndex + (newText?.length ?? 0); + endIndex = currentIndex + newText.length; } } return { startIndex, endIndex, - diff: newText?.substring(startIndex, endIndex) ?? '', + diff: newText.substring(startIndex, endIndex), }; }, [selection.start, selection.end], @@ -375,7 +375,7 @@ function ComposerWithSuggestions( text: newComment, emojis, cursorPosition, - } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue ?? '', preferredSkinTone, preferredLocale); + } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); if (emojis.length) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis.length) { @@ -748,7 +748,7 @@ function ComposerWithSuggestions( isComposerFullSize={isComposerFullSize} value={value} testID="composer" - numberOfLines={numberOfLines ?? 0} + numberOfLines={numberOfLines ?? undefined} onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition onLayout={onLayout} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index 0fa535329d33..7f169ef15918 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -16,7 +16,7 @@ function IncrementRenderCount() { return null; } -function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { +function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { // Eventually Auto focus on e2e tests useEffect(() => { const testConfig = E2EClient.getCurrentActiveTestConfig(); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 98630ff88b23..283f0dfab18f 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -289,9 +289,7 @@ function ReportActionCompose({ */ const submitForm = useCallback( (event?: SyntheticEvent) => { - if (event) { - event.preventDefault(); - } + event?.preventDefault(); const newComment = composerRef.current?.prepareCommentAndResetComposer(); if (!newComment) { diff --git a/src/pages/home/report/ReportActionCompose/SendButton.tsx b/src/pages/home/report/ReportActionCompose/SendButton.tsx index 4726c1638f42..c505eb0e32e7 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.tsx +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -23,12 +23,9 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP const styles = useThemeStyles(); const {translate} = useLocalize(); - const Tap = Gesture.Tap() - // @ts-expect-error Enabled require argument but when passing something button is not working - .enabled() - .onEnd(() => { - handleSendMessage(); - }); + const Tap = Gesture.Tap().onEnd(() => { + handleSendMessage(); + }); return ( { updateComment(comment ?? ''); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index 206c91ed9b08..0ae45d2d705d 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -28,7 +28,7 @@ type SuggestionEmojiOnyxProps = { type SuggestionEmojiProps = SuggestionProps & SuggestionEmojiOnyxProps & { /** Function to clear the input */ - resetKeyboardInput: (() => void) | undefined; + resetKeyboardInput?: () => void; }; /** diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index e484ce9886a7..3ff8f2b0eb8e 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -41,7 +41,7 @@ function ReportTypingIndicator({userTypingStatuses}: ReportTypingIndicatorProps) if (usersTyping.length === 1) { return ( Date: Fri, 9 Feb 2024 16:32:51 +0100 Subject: [PATCH 085/297] fix: conflicts --- src/libs/NextStepUtils.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 3b42382b10f9..e99f2c2ef8a1 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,5 +1,6 @@ import {format, lastDayOfMonth, setDate} from 'date-fns'; import Str from 'expensify-common/lib/str'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -62,18 +63,21 @@ type BuildNextStepParameters = { * @param parameters.isPaidWithWallet - Whether a report has been paid with the wallet or outside of Expensify * @returns nextStep */ -function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { +function buildNextStep( + report: OnyxEntry | EmptyObject, + predictedNextStatus: ValueOf, + {isPaidWithWallet}: BuildNextStepParameters = {}, +): ReportNextStep | null { if (!ReportUtils.isExpenseReport(report)) { return null; } - const {policyID = '', ownerAccountID = -1, managerID = -1} = report; - const policy = ReportUtils.getPolicy(policyID); + const policy = ReportUtils.getPolicy(report?.policyID); const {submitsTo, harvesting, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; - const isOwner = currentUserAccountID === ownerAccountID; - const isManager = currentUserAccountID === managerID; + const isOwner = currentUserAccountID === report?.ownerAccountID; + const isManager = currentUserAccountID === report?.managerID; const isSelfApproval = currentUserAccountID === submitsTo; - const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; + const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([report?.ownerAccountID ?? -1])[0] ?? ''; const managerDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(submitsTo) ?? ''; const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; From f69fce5038eb5366ec3ada2f96050edc13382306 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 12 Feb 2024 09:52:40 +0100 Subject: [PATCH 086/297] fix: typecheck --- src/libs/OptionsListUtils.ts | 1 + src/types/onyx/index.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 8a96c2a8705c..863970dbda30 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -30,6 +30,7 @@ import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 6343b9a1b2ec..51e772c0d173 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -50,6 +50,7 @@ import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type {PolicyReportField, PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; +import type {PolicyTaxRate, PolicyTaxRates} from './PolicyTaxRates'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -169,8 +170,6 @@ export type { PolicyReportField, PolicyReportFields, RecentlyUsedReportFields, - PolicyTaxRate, - PolicyTaxRates, LastPaymentMethod, NewRoomForm, IKnowATeacherForm, @@ -180,4 +179,6 @@ export type { ReportFieldEditForm, RoomNameForm, PaymentMethodType, + PolicyTaxRate, + PolicyTaxRates, }; From 7786efc13997918b27fa488be2a19d854140c5af Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 13 Feb 2024 12:27:21 +0100 Subject: [PATCH 087/297] feat: upgrade react-native-reanimated --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11099886bfb0..cf598969e55e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,7 +105,7 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "^3.6.1", + "react-native-reanimated": "^3.7.0", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.7.4", "react-native-screens": "3.29.0", @@ -45184,9 +45184,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz", - "integrity": "sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.7.0.tgz", + "integrity": "sha512-KM+MKa3CJWqsF4GlOLLKBxTR2NEcrg5/HP9J2b6Dfgvll1sjZPywCOEEIh967SboEU8N9LjYZuoVm2UoXGxp2Q==", "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", diff --git a/package.json b/package.json index 71983e0e1679..a92a633f7e15 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "^3.6.1", + "react-native-reanimated": "^3.7.0", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.7.4", "react-native-screens": "3.29.0", From 68f4cf96945713268d8f1d34b9c4c81dd2ca2bba Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 14 Feb 2024 15:03:53 +0100 Subject: [PATCH 088/297] fix: typecheck --- src/ONYXKEYS.ts | 2 +- src/components/MoneyRequestConfirmationList.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0735bc53e56c..944788d99cb8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -453,7 +453,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; - [ONYXKEYS.COLLECTION.POLICY_TAX_RATE]: string[]; + [ONYXKEYS.COLLECTION.POLICY_TAX_RATE]: OnyxTypes.PolicyTaxRate; }; type OnyxValuesMapping = { diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 1dd6e9b9f302..de289f1fba3b 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -77,6 +77,9 @@ type MoneyRequestConfirmationListOnyxProps = { /** The policy of root parent report */ policy: OnyxEntry; + + /** The session of the logged in user */ + session: OnyxEntry; }; type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & @@ -818,7 +821,7 @@ function MoneyRequestConfirmationList({ Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( CONST.IOU.ACTION.CREATE, - CONST.IOU.TYPE.SEND, + iouType, transaction?.transactionID ?? '', reportID ?? '', Navigation.getActiveRouteWithoutParams(), @@ -905,5 +908,8 @@ export default withCurrentUserPersonalDetails( iou: { key: ONYXKEYS.IOU, }, + session: { + key: ONYXKEYS.SESSION, + }, })(MoneyRequestConfirmationList), ); From 022db8f5be1264e9fb45a7393c84ab2d309a97af Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 14 Feb 2024 16:45:57 +0100 Subject: [PATCH 089/297] fix: resolve comments --- src/components/AttachmentModal.tsx | 4 ++-- .../AddCommentOrAttachementParams.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Report.ts | 4 ++-- .../ComposerWithSuggestions.tsx | 10 ++++---- .../ReportActionCompose.tsx | 24 ++++--------------- .../SilentCommentUpdater/index.tsx | 1 + src/types/modules/pusher.d.ts | 2 +- src/types/onyx/ReportAction.ts | 2 +- 9 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 2b4afc037796..7f0178863fc9 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -89,7 +89,7 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { source?: AvatarSource; /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm?: ((file: Partial) => void) | null; + onConfirm?: ((file: FileObject) => void) | null; /** Whether the modal should be open by default */ defaultOpen?: boolean; @@ -264,7 +264,7 @@ function AttachmentModal({ } if (onConfirm) { - onConfirm(Object.assign(file ?? {}, {source: sourceState})); + onConfirm(Object.assign(file ?? {}, {source: sourceState} as FileObject)); } setIsModalOpen(false); diff --git a/src/libs/API/parameters/AddCommentOrAttachementParams.ts b/src/libs/API/parameters/AddCommentOrAttachementParams.ts index 4eab35be7dd2..a705c92f7f27 100644 --- a/src/libs/API/parameters/AddCommentOrAttachementParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachementParams.ts @@ -5,7 +5,7 @@ type AddCommentOrAttachementParams = { reportActionID?: string; commentReportActionID?: string | null; reportComment?: string; - file?: Partial; + file?: FileObject; timezone?: string; shouldAllowActionableMentionWhispers?: boolean; clientCreatedTime?: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2b073b110184..ed3dc16b56a4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2631,7 +2631,7 @@ function getReportDescriptionText(report: Report): string { return parser.htmlToText(report.description); } -function buildOptimisticAddCommentReportAction(text?: string, file?: Partial, actorAccountID?: number): OptimisticReportAction { +function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); const isAttachment = !text && file !== undefined; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bfae6ece1fa1..7127178daae7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -353,7 +353,7 @@ function notifyNewAction(reportID: string, accountID?: number, reportActionID?: * - Adding one attachment * - Add both a comment and attachment simultaneously */ -function addActions(reportID: string, text = '', file?: Partial) { +function addActions(reportID: string, text = '', file?: FileObject) { let reportCommentText = ''; let reportCommentAction: OptimisticAddCommentReportAction | undefined; let attachmentAction: OptimisticAddCommentReportAction | undefined; @@ -512,7 +512,7 @@ function addActions(reportID: string, text = '', file?: Partial) { } /** Add an attachment and optional comment. */ -function addAttachment(reportID: string, file: Partial, text = '') { +function addAttachment(reportID: string, file: FileObject, text = '') { addActions(reportID, text, file); } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index cfe45e5a011b..a345926ad7d2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -214,6 +214,7 @@ function ComposerWithSuggestions( onFocus, onBlur, onValueChange, + // Composer isComposerFullSize, isMenuVisible, @@ -232,11 +233,13 @@ function ComposerWithSuggestions( listHeight, isScrollLikelyLayoutTriggered, raiseIsScrollLikelyLayoutTriggered, + // Refs suggestionsRef, animatedRef, isNextModalWillOpenRef, editFocused, + // For testing children, }: ComposerWithSuggestionsProps, @@ -278,7 +281,7 @@ function ComposerWithSuggestions( const syncSelectionWithOnChangeTextRef = useRef(null); - const suggestions = suggestionsRef.current?.getSuggestions() ?? (() => []); + const suggestions = suggestionsRef.current?.getSuggestions() ?? []; const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions?.length ?? 0); @@ -457,7 +460,7 @@ function ComposerWithSuggestions( [reportID, numberOfLines], ); - const prepareCommentAndResetComposer = useCallback(() => { + const prepareCommentAndResetComposer = useCallback((): string => { const trimmedComment = commentRef.current.trim(); const commentLength = ReportUtils.getCommentLength(trimmedComment); @@ -575,7 +578,6 @@ function ComposerWithSuggestions( /** * Focus the composer text input * @param [shouldDelay=false] Impose delay before focusing the composer - * @memberof ReportActionCompose */ const focus = useCallback((shouldDelay = false) => { focusComposerWithDelay(textInputRef.current)(shouldDelay); @@ -614,7 +616,7 @@ function ComposerWithSuggestions( } // if we're typing on another input/text area, do not focus - if (['INPUT', 'TEXTAREA'].includes((e.target as Element)?.nodeName)) { + if (['INPUT', 'TEXTAREA'].includes((e.target as Element | null)?.nodeName ?? '')) { return; } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 735fb323d638..84f7600f8ef0 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -43,6 +43,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems'; import ComposerWithSuggestions from './ComposerWithSuggestions'; +import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions/ComposerWithSuggestions'; import SendButton from './SendButton'; type ComposerRef = { @@ -71,36 +72,19 @@ type ReportActionComposeOnyxProps = { }; type ReportActionComposeProps = ReportActionComposeOnyxProps & - WithCurrentUserPersonalDetailsProps & { + WithCurrentUserPersonalDetailsProps & + Pick & { /** A method to call when the form is submitted */ onSubmit: (newComment: string | undefined) => void; - /** The ID of the report actions will be created for */ - reportID: string; - /** The report currently being looked at */ report: OnyxEntry; - /** Is composer full size */ - isComposerFullSize?: boolean; - - /** Whether user interactions should be disabled */ - disabled?: boolean; - - /** Height of the list which the composer is part of */ - listHeight?: number; - /** The type of action that's pending */ pendingAction?: OnyxCommon.PendingAction; /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; - - /** Whether the chat is empty */ - isEmptyChat?: boolean; - - /** The last report action */ - lastReportAction?: OnyxTypes.ReportAction; }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will @@ -268,7 +252,7 @@ function ReportActionCompose({ }, []); const addAttachment = useCallback( - (file: Partial) => { + (file: FileObject) => { playSound(SOUNDS.DONE); const newComment = composerRef?.current?.prepareCommentAndResetComposer(); Report.addAttachment(reportID, file, newComment); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index c84bd3786610..1abc6567bc7b 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -43,5 +43,6 @@ SilentCommentUpdater.displayName = 'SilentCommentUpdater'; export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, + initialValue: '', }, })(SilentCommentUpdater); diff --git a/src/types/modules/pusher.d.ts b/src/types/modules/pusher.d.ts index 676d7a7ee2fc..e9aa50085e8d 100644 --- a/src/types/modules/pusher.d.ts +++ b/src/types/modules/pusher.d.ts @@ -9,6 +9,6 @@ declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface File { source: string; - uri: string; + uri?: string; } } diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 7071c211c2e9..d339d666a411 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -169,7 +169,7 @@ type ReportActionBase = { isFirstItem?: boolean; /** Informations about attachments of report action */ - attachmentInfo?: Partial | EmptyObject; + attachmentInfo?: FileObject | EmptyObject; /** Receipt tied to report action */ receipt?: Receipt; From 22251e6f10b7e87f870b10b48f9c48e57c57580e Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Wed, 14 Feb 2024 21:45:40 +0530 Subject: [PATCH 090/297] fix receipt upload error when file name contain spaces --- src/libs/actions/IOU.ts | 7 ++++--- src/libs/fileDownload/FileUtils.ts | 6 ++++-- src/libs/fileDownload/types.ts | 2 +- src/pages/iou/request/step/IOURequestStepConfirmation.js | 5 +++-- src/pages/iou/request/step/IOURequestStepParticipants.js | 5 +++-- .../iou/request/step/IOURequestStepScan/index.native.js | 4 +++- src/types/onyx/Transaction.ts | 1 + 7 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7fca6614f1a1..3473cc567c32 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -324,9 +324,9 @@ function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } -function setMoneyRequestReceipt(transactionID: string, source: string, filename: string, isDraft: boolean) { +function setMoneyRequestReceipt(transactionID: string, source: string, filename: string, isDraft: boolean, type?: string) { Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { - receipt: {source}, + receipt: {source, type: type ?? ''}, filename, }); } @@ -3824,6 +3824,7 @@ function navigateToStartStepIfScanFileCannotBeRead( iouType: ValueOf, transactionID: string, reportID: string, + receiptType: string, ) { if (!receiptFilename || !receiptPath) { return; @@ -3837,7 +3838,7 @@ function navigateToStartStepIfScanFileCannotBeRead( } IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID); }; - FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure); + FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure, receiptType); } /** Save the preferred payment method for a policy */ diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 055abf140e64..d0ecd7c51e6a 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -159,7 +159,7 @@ function appendTimeToFileName(fileName: string): string { * @param path - the blob url of the locally uploaded file * @param fileName - name of the file to read */ -const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}) => +const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}, fileType = '') => new Promise((resolve) => { if (!path) { resolve(); @@ -176,7 +176,9 @@ const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () } res.blob() .then((blob) => { - const file = new File([blob], cleanFileName(fileName), {type: blob.type}); + // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file. + // In this case, let us fallback on fileType provided by the caller of this function. + const file = new File([blob], cleanFileName(fileName), {type: blob.type ? blob.type : fileType}); file.source = path; // For some reason, the File object on iOS does not have a uri property // so images aren't uploaded correctly to the backend diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index 6d92bddd5816..9a62e775a6ea 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -8,7 +8,7 @@ type GetImageResolution = (url: File | Asset) => Promise; type ExtensionAndFileName = {fileName: string; fileExtension: string}; type SplitExtensionFromFileName = (fileName: string) => ExtensionAndFileName; -type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void) => Promise; +type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void, fileType: string) => Promise; type AttachmentDetails = { previewSourceURL: null | string; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index cf4e6a461bc1..d533131c2ede 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -92,6 +92,7 @@ function IOURequestStepConfirmation({ const [receiptFile, setReceiptFile] = useState(); const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); + const receiptType = lodashGet(transaction, 'receipt.type'); const transactionTaxCode = transaction.taxRate && transaction.taxRate.keyForList; const transactionTaxAmount = transaction.taxAmount; const requestType = TransactionUtils.getRequestType(transaction); @@ -171,8 +172,8 @@ function IOURequestStepConfirmation({ setReceiptFile(receipt); }; - IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID); - }, [receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); + IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); + }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); useEffect(() => { const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 5f1b22cab128..29719313563e 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -42,13 +42,14 @@ function IOURequestStepParticipants({ const headerTitle = translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); + const receiptType = lodashGet(transaction, 'receipt.type'); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then // the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process. useEffect(() => { - IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID); - }, [receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]); + IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID, receiptType); + }, [receiptType, receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]); const addParticipant = useCallback( (val) => { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index b23420b5ef69..181d8edc22f2 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -211,7 +211,9 @@ function IOURequestStepScan({ } // Store the receipt on the transaction object in Onyx - IOU.setMoneyRequestReceipt(transactionID, file.uri, file.name, action !== CONST.IOU.ACTION.EDIT); + // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file. + // So, let us also save the file type in receipt for later use during blob fetch + IOU.setMoneyRequestReceipt(transactionID, file.uri, file.name, action !== CONST.IOU.ACTION.EDIT, file.type); if (action === CONST.IOU.ACTION.EDIT) { updateScanAndNavigate(file, file.uri); diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index b559346a48de..c4fb2af36002 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -47,6 +47,7 @@ type Receipt = { source?: ReceiptSource; filename?: string; state?: ValueOf; + type?: string; }; type Route = { From 49a0f0b50036a65c6442e77c15c9adc28e95f612 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 15 Feb 2024 00:53:14 +0800 Subject: [PATCH 091/297] prettier --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e56d15cccc24..ca220965df48 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1,3 +1,4 @@ +import {ParamListBase, StackNavigationState} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import {format} from 'date-fns'; import fastMerge from 'expensify-common/lib/fastMerge'; @@ -59,7 +60,6 @@ import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as Policy from './Policy'; import * as Report from './Report'; -import { ParamListBase, StackNavigationState } from '@react-navigation/native'; type MoneyRequestRoute = StackScreenProps['route']; From e5751da59a3f651050c5f757ebc555f16f246642 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 15 Feb 2024 00:57:34 +0800 Subject: [PATCH 092/297] lint --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ca220965df48..c3a3abc7368a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1,4 +1,4 @@ -import {ParamListBase, StackNavigationState} from '@react-navigation/native'; +import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import {format} from 'date-fns'; import fastMerge from 'expensify-common/lib/fastMerge'; From fe5465074a8ab1acfbc6b207965e4cad4e093e03 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 15 Feb 2024 14:55:53 +0700 Subject: [PATCH 093/297] lint fix --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 589bb7bff15b..f6fb82990968 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -377,7 +377,7 @@ function needsToBeManuallySubmitted(iouReport: OnyxTypes.Report) { /** * Return the object to update hasOutstandingChildRequest */ -function getOutstandingChildRequest(policy: OnyxEntry | EmptyObject, iouReport: OnyxTypes.Report) { +function getOutstandingChildRequest(policy: OnyxEntry | EmptyObject, iouReport: OnyxTypes.Report): OutstandingChildRequest { if (!needsToBeManuallySubmitted(iouReport)) { return { hasOutstandingChildRequest: false, From bee6bd1c9b91f5bb2937de1f857f4b272988336c Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 15 Feb 2024 18:40:31 +0700 Subject: [PATCH 094/297] fix error message in LHN --- src/components/ReportActionItem/MoneyRequestView.tsx | 8 +------- src/libs/OptionsListUtils.ts | 2 +- src/libs/TransactionUtils.ts | 8 ++++++++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index ae7144e9c6a4..6530279a54ab 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -189,13 +189,7 @@ function MoneyRequestView({ const hasReceipt = TransactionUtils.hasReceipt(transaction); let receiptURIs; - const hasErrors = Boolean( - canEdit && - transaction && - !TransactionUtils.isDistanceRequest(transaction) && - !TransactionUtils.isReceiptBeingScanned(transaction) && - TransactionUtils.areRequiredFieldsEmpty(transaction), - ); + const hasErrors = Boolean(canEdit && TransactionUtils.hasMissingRequiredFields(transaction)); if (hasReceipt) { receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction); } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b6518b361381..33d97280a5ac 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -482,7 +482,7 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { + if (TransactionUtils.hasMissingRequiredFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); } } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8a814f311481..9bd4b19df3be 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -435,6 +435,13 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean return Boolean(transaction && hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction)); } +/** + * Check if the transaction has missing required fields + */ +function hasMissingRequiredFields(transaction: OnyxEntry): boolean { + return Boolean(transaction && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction)); +} + /** * Check if the transaction has a defined route */ @@ -592,4 +599,5 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, + hasMissingRequiredFields, }; From 6215e7678218f33a7e3fbb1009d53753f0617e2a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 15 Feb 2024 18:49:36 +0700 Subject: [PATCH 095/297] fix remove redundant boolean --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d5b836fb5318..e8ac5162c7e9 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -189,7 +189,7 @@ function MoneyRequestView({ const hasReceipt = TransactionUtils.hasReceipt(transaction); let receiptURIs; - const hasErrors = Boolean(canEdit && TransactionUtils.hasMissingRequiredFields(transaction)); + const hasErrors = canEdit && TransactionUtils.hasMissingRequiredFields(transaction); if (hasReceipt) { receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction); } From d7708c104d60b0295cb382c617646e3820d9b4e7 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 15 Feb 2024 14:33:50 +0100 Subject: [PATCH 096/297] fix: removed uncessesary condition --- .../MoneyRequestConfirmationList.tsx | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index de289f1fba3b..8430dd66d3c8 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -262,7 +262,7 @@ function MoneyRequestConfirmationList({ const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledOptions(Object.values(policyTagList ?? {}))); // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = isPolicyExpenseChat && policy?.isTaxTrackingEnabled; + const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled); // A flag for showing the billable field const shouldShowBillable = !policy?.disabledFields?.defaultBillable ?? true; @@ -457,7 +457,7 @@ function MoneyRequestConfirmationList({ translate, toLocaleDigit, ); - IOU.setMoneyRequestMerchant_temporaryForRefactor(transactionID ?? '', distanceMerchant); + IOU.setMoneyRequestMerchant(transactionID ?? '', distanceMerchant, false); }, [hasRoute, distance, mileageRate?.unit, mileageRate?.rate, mileageRate?.currency, translate, toLocaleDigit, isDistanceRequest, transactionID, isDistanceRequestWithPendingRoute]); const selectParticipant = useCallback( @@ -762,11 +762,15 @@ function MoneyRequestConfirmationList({ style={styles.moneyRequestMenuItem} titleStyle={styles.flex1} onPress={() => { - if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID ?? '', reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.MERCHANT)); - return; - } - Navigation.navigate(ROUTES.MONEY_REQUEST_MERCHANT.getRoute(iouType, reportID)); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( + CONST.IOU.ACTION.EDIT, + iouType, + transactionID ?? '', + reportID ?? '', + Navigation.getActiveRouteWithoutParams(), + ), + ); }} disabled={didConfirm} interactive={!isReadOnly} @@ -805,22 +809,9 @@ function MoneyRequestConfirmationList({ description={policyTagListName} numberOfLinesTitle={2} onPress={() => { - if (isEditingSplitBill) { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( - CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.SPLIT, - transaction?.transactionID ?? '', - reportID ?? '', - Navigation.getActiveRouteWithoutParams(), - ), - ); - return; - } - Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( - CONST.IOU.ACTION.CREATE, + CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', reportID ?? '', From b4a2e1b1a750a6d3aed2a2290dae2fadf4e7fd07 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 16 Feb 2024 12:47:26 +0100 Subject: [PATCH 097/297] Migrate RoomNameInput to TypeScript --- .../{index.native.js => index.native.tsx} | 55 ++++++++++------- .../RoomNameInput/{index.js => index.tsx} | 59 ++++++++++++------- .../RoomNameInput/roomNameInputPropTypes.js | 58 ------------------ src/components/RoomNameInput/types.ts | 22 +++++++ 4 files changed, 94 insertions(+), 100 deletions(-) rename src/components/RoomNameInput/{index.native.js => index.native.tsx} (57%) rename src/components/RoomNameInput/{index.js => index.tsx} (62%) delete mode 100644 src/components/RoomNameInput/roomNameInputPropTypes.js create mode 100644 src/components/RoomNameInput/types.ts diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.tsx similarity index 57% rename from src/components/RoomNameInput/index.native.js rename to src/components/RoomNameInput/index.native.tsx index bae347fca3d2..f8055b71fe84 100644 --- a/src/components/RoomNameInput/index.native.js +++ b/src/components/RoomNameInput/index.native.tsx @@ -1,26 +1,41 @@ +import type {ForwardedRef} from 'react'; import React from 'react'; -import _ from 'underscore'; +import type {NativeSyntheticEvent, TextInputChangeEventData} from 'react-native'; import TextInput from '@components/TextInput'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import getOperatingSystem from '@libs/getOperatingSystem'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; -import * as roomNameInputPropTypes from './roomNameInputPropTypes'; +import type RoomNameInputProps from './types'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, onSubmitEditing, returnKeyType, shouldDelayFocus}) { +function RoomNameInput({ + isFocused, + autoFocus, + disabled, + errorText, + forwardedRef, + value, + onBlur, + onChangeText, + onInputChange, + onSubmitEditing, + returnKeyType, + shouldDelayFocus, +}: RoomNameInputProps) { const {translate} = useLocalize(); /** * Calls the onChangeText callback with a modified room name - * @param {Event} event + * @param event */ - const setModifiedRoomName = (event) => { + const setModifiedRoomName = (event: NativeSyntheticEvent) => { const roomName = event.nativeEvent.text; const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); - onChangeText(modifiedRoomName); + onChangeText?.(modifiedRoomName); // if custom component has onInputChange, use it to trigger changes (Form input) - if (_.isFunction(onInputChange)) { + if (typeof onInputChange === 'function') { onInputChange(modifiedRoomName); } }; @@ -33,15 +48,15 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, disabled={disabled} label={translate('newRoomPage.roomName')} accessibilityLabel={translate('newRoomPage.roomName')} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} prefixCharacter={CONST.POLICY.ROOM_PREFIX} placeholder={translate('newRoomPage.social')} onChange={setModifiedRoomName} - value={value.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice. + value={value?.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice. errorText={errorText} maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 - onBlur={(event) => isFocused && onBlur(event)} + onBlur={(event) => isFocused && onBlur?.(event)} onSubmitEditing={onSubmitEditing} returnKeyType={returnKeyType} autoFocus={isFocused && autoFocus} @@ -51,18 +66,18 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, ); } -RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; -RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; RoomNameInput.displayName = 'RoomNameInput'; -const RoomNameInputWithRef = React.forwardRef((props, ref) => ( - -)); +function RoomNameInputWithRef(props: Omit, ref: ForwardedRef) { + return ( + + ); +} RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; -export default RoomNameInputWithRef; +export default React.forwardRef(RoomNameInputWithRef); diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.tsx similarity index 62% rename from src/components/RoomNameInput/index.js rename to src/components/RoomNameInput/index.tsx index e3c5a86ff945..b82927322bec 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.tsx @@ -1,27 +1,41 @@ import React, {useState} from 'react'; -import _ from 'underscore'; +import type {ForwardedRef} from 'react'; +import type {NativeSyntheticEvent, TextInputChangeEventData} from 'react-native'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; +import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types'; import CONST from '@src/CONST'; -import * as roomNameInputPropTypes from './roomNameInputPropTypes'; +import type RoomNameInputProps from './types'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, onSubmitEditing, returnKeyType, shouldDelayFocus}) { +function RoomNameInput({ + isFocused, + autoFocus, + disabled, + errorText, + forwardedRef, + value, + onBlur, + onChangeText, + onInputChange, + onSubmitEditing, + returnKeyType, + shouldDelayFocus, + inputID, +}: RoomNameInputProps) { const {translate} = useLocalize(); - - const [selection, setSelection] = useState(); + const [selection, setSelection] = useState<{start: number; end: number}>({start: 0, end: 0}); /** * Calls the onChangeText callback with a modified room name - * @param {Event} event */ - const setModifiedRoomName = (event) => { + const setModifiedRoomName = (event: NativeSyntheticEvent) => { const roomName = event.nativeEvent.text; const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); - onChangeText(modifiedRoomName); + onChangeText?.(modifiedRoomName); // if custom component has onInputChange, use it to trigger changes (Form input) - if (_.isFunction(onInputChange)) { + if (typeof onInputChange === 'function') { onInputChange(modifiedRoomName); } @@ -30,7 +44,7 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, // If it is, then the room name is valid (does not contain forbidden characters) – no action required // If not, then the room name contains invalid characters, and we must adjust the cursor position manually // Read more: https://github.com/Expensify/App/issues/12741 - const oldRoomNameWithHash = value || ''; + const oldRoomNameWithHash = value ?? ''; const newRoomNameWithHash = `${CONST.POLICY.ROOM_PREFIX}${roomName}`; if (modifiedRoomName !== newRoomNameWithHash) { const offset = modifiedRoomName.length - oldRoomNameWithHash.length; @@ -43,20 +57,21 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, setSelection(event.nativeEvent.selection)} onSubmitEditing={onSubmitEditing} returnKeyType={returnKeyType} errorText={errorText} autoCapitalize="none" - onBlur={(event) => isFocused && onBlur(event)} + onBlur={(event) => isFocused && onBlur?.(event)} shouldDelayFocus={shouldDelayFocus} autoFocus={isFocused && autoFocus} maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} @@ -66,18 +81,18 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, ); } -RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; -RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; RoomNameInput.displayName = 'RoomNameInput'; -const RoomNameInputWithRef = React.forwardRef((props, ref) => ( - -)); +function RoomNameInputWithRef(props: Omit, ref: ForwardedRef) { + return ( + + ); +} RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; -export default RoomNameInputWithRef; +export default React.forwardRef(RoomNameInputWithRef); diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js deleted file mode 100644 index aa547354a4c5..000000000000 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ /dev/null @@ -1,58 +0,0 @@ -import PropTypes from 'prop-types'; -import refPropTypes from '@components/refPropTypes'; -import {translatableTextPropTypes} from '@libs/Localize'; - -const propTypes = { - /** Callback to execute when the text input is modified correctly */ - onChangeText: PropTypes.func, - - /** Room name to show in input field. This should include the '#' already prefixed to the name */ - value: PropTypes.string, - - /** Whether we should show the input as disabled */ - disabled: PropTypes.bool, - - /** Error text to show */ - errorText: translatableTextPropTypes, - - /** A ref forwarded to the TextInput */ - forwardedRef: refPropTypes, - - /** On submit editing handler provided by the FormProvider */ - onSubmitEditing: PropTypes.func, - - /** Return key type provided to the TextInput */ - returnKeyType: PropTypes.string, - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** AutoFocus */ - autoFocus: PropTypes.bool, - - /** Whether we should wait before focusing the TextInput, useful when using transitions on Android */ - shouldDelayFocus: PropTypes.bool, - - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, -}; - -const defaultProps = { - onChangeText: () => {}, - value: '', - disabled: false, - errorText: '', - forwardedRef: () => {}, - onSubmitEditing: () => {}, - returnKeyType: undefined, - - inputID: undefined, - onBlur: () => {}, - autoFocus: false, - shouldDelayFocus: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/RoomNameInput/types.ts b/src/components/RoomNameInput/types.ts new file mode 100644 index 000000000000..6c582e5d07f6 --- /dev/null +++ b/src/components/RoomNameInput/types.ts @@ -0,0 +1,22 @@ +import type {ForwardedRef} from 'react'; +import type {NativeSyntheticEvent, ReturnKeyTypeOptions, TextInputFocusEventData, TextInputSubmitEditingEventData} from 'react-native'; +import type {MaybePhraseKey} from '@libs/Localize'; +import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types'; + +type RoomNameInputProps = { + value?: string; + disabled?: boolean; + errorText?: MaybePhraseKey; + onChangeText?: (value: string) => void; + forwardedRef?: ForwardedRef; + onSubmitEditing?: (event: NativeSyntheticEvent) => void; + onInputChange?: (value: string) => void; + returnKeyType?: ReturnKeyTypeOptions; + inputID?: string; + onBlur?: (event: NativeSyntheticEvent) => void; + autoFocus?: boolean; + shouldDelayFocus?: boolean; + isFocused?: boolean; +}; + +export default RoomNameInputProps; From c46a4b4c74519f7aa94d083196b7c63b6d88511d Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 16 Feb 2024 14:45:34 +0100 Subject: [PATCH 098/297] Add missing inputID to native --- src/components/RoomNameInput/index.native.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/RoomNameInput/index.native.tsx b/src/components/RoomNameInput/index.native.tsx index f8055b71fe84..6651acdf2061 100644 --- a/src/components/RoomNameInput/index.native.tsx +++ b/src/components/RoomNameInput/index.native.tsx @@ -22,6 +22,7 @@ function RoomNameInput({ onSubmitEditing, returnKeyType, shouldDelayFocus, + inputID, }: RoomNameInputProps) { const {translate} = useLocalize(); @@ -46,6 +47,7 @@ function RoomNameInput({ Date: Fri, 16 Feb 2024 15:34:36 +0100 Subject: [PATCH 099/297] Add RoomNameInput type to ValidInputs --- src/components/Form/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index cb2740fdafe5..1e92f106b459 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -7,6 +7,7 @@ import type AmountTextInput from '@components/AmountTextInput'; import type CheckboxWithLabel from '@components/CheckboxWithLabel'; import type CountrySelector from '@components/CountrySelector'; import type Picker from '@components/Picker'; +import type RoomNameInput from '@components/RoomNameInput'; import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import type StatePicker from '@components/StatePicker'; import type TextInput from '@components/TextInput'; @@ -32,7 +33,8 @@ type ValidInputs = | typeof CountrySelector | typeof AmountForm | typeof BusinessTypePicker - | typeof StatePicker; + | typeof StatePicker + | typeof RoomNameInput; type ValueTypeKey = 'string' | 'boolean' | 'date'; type ValueTypeMap = { From 6a7e8df74d649463844bf90b9755de2441db904c Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 16 Feb 2024 16:04:29 +0100 Subject: [PATCH 100/297] Remove forwardedRef from props --- src/components/RoomNameInput/index.native.tsx | 44 +++------------- src/components/RoomNameInput/index.tsx | 50 ++++--------------- src/components/RoomNameInput/types.ts | 3 -- 3 files changed, 17 insertions(+), 80 deletions(-) diff --git a/src/components/RoomNameInput/index.native.tsx b/src/components/RoomNameInput/index.native.tsx index 6651acdf2061..60fc6ade0082 100644 --- a/src/components/RoomNameInput/index.native.tsx +++ b/src/components/RoomNameInput/index.native.tsx @@ -9,21 +9,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import type RoomNameInputProps from './types'; -function RoomNameInput({ - isFocused, - autoFocus, - disabled, - errorText, - forwardedRef, - value, - onBlur, - onChangeText, - onInputChange, - onSubmitEditing, - returnKeyType, - shouldDelayFocus, - inputID, -}: RoomNameInputProps) { +function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInputChange, ...props}: RoomNameInputProps, ref: ForwardedRef) { const {translate} = useLocalize(); /** @@ -45,41 +31,25 @@ function RoomNameInput({ return ( isFocused && onBlur?.(event)} - onSubmitEditing={onSubmitEditing} - returnKeyType={returnKeyType} autoFocus={isFocused && autoFocus} autoCapitalize="none" - shouldDelayFocus={shouldDelayFocus} + onChange={setModifiedRoomName} + keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 /> ); } RoomNameInput.displayName = 'RoomNameInput'; -function RoomNameInputWithRef(props: Omit, ref: ForwardedRef) { - return ( - - ); -} - -RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; - -export default React.forwardRef(RoomNameInputWithRef); +export default React.forwardRef(RoomNameInput); diff --git a/src/components/RoomNameInput/index.tsx b/src/components/RoomNameInput/index.tsx index b82927322bec..a6b8e4d83091 100644 --- a/src/components/RoomNameInput/index.tsx +++ b/src/components/RoomNameInput/index.tsx @@ -8,21 +8,7 @@ import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/typ import CONST from '@src/CONST'; import type RoomNameInputProps from './types'; -function RoomNameInput({ - isFocused, - autoFocus, - disabled, - errorText, - forwardedRef, - value, - onBlur, - onChangeText, - onInputChange, - onSubmitEditing, - returnKeyType, - shouldDelayFocus, - inputID, -}: RoomNameInputProps) { +function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInputChange, ...props}: RoomNameInputProps, ref: ForwardedRef) { const {translate} = useLocalize(); const [selection, setSelection] = useState<{start: number; end: number}>({start: 0, end: 0}); @@ -55,26 +41,22 @@ function RoomNameInput({ return ( setSelection(event.nativeEvent.selection)} - onSubmitEditing={onSubmitEditing} - returnKeyType={returnKeyType} - errorText={errorText} - autoCapitalize="none" + maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} onBlur={(event) => isFocused && onBlur?.(event)} - shouldDelayFocus={shouldDelayFocus} autoFocus={isFocused && autoFocus} - maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} + autoCapitalize="none" + onChange={setModifiedRoomName} + onSelectionChange={(event) => setSelection(event.nativeEvent.selection)} + selection={selection} spellCheck={false} shouldInterceptSwipe /> @@ -83,16 +65,4 @@ function RoomNameInput({ RoomNameInput.displayName = 'RoomNameInput'; -function RoomNameInputWithRef(props: Omit, ref: ForwardedRef) { - return ( - - ); -} - -RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; - -export default React.forwardRef(RoomNameInputWithRef); +export default React.forwardRef(RoomNameInput); diff --git a/src/components/RoomNameInput/types.ts b/src/components/RoomNameInput/types.ts index 6c582e5d07f6..f90051de6fd0 100644 --- a/src/components/RoomNameInput/types.ts +++ b/src/components/RoomNameInput/types.ts @@ -1,14 +1,11 @@ -import type {ForwardedRef} from 'react'; import type {NativeSyntheticEvent, ReturnKeyTypeOptions, TextInputFocusEventData, TextInputSubmitEditingEventData} from 'react-native'; import type {MaybePhraseKey} from '@libs/Localize'; -import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types'; type RoomNameInputProps = { value?: string; disabled?: boolean; errorText?: MaybePhraseKey; onChangeText?: (value: string) => void; - forwardedRef?: ForwardedRef; onSubmitEditing?: (event: NativeSyntheticEvent) => void; onInputChange?: (value: string) => void; returnKeyType?: ReturnKeyTypeOptions; From 5094741a8ea30bab996239e46269cb2b7213ad48 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 16 Feb 2024 16:14:24 +0100 Subject: [PATCH 101/297] Add default values --- src/components/RoomNameInput/index.native.tsx | 7 ++++++- src/components/RoomNameInput/index.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/RoomNameInput/index.native.tsx b/src/components/RoomNameInput/index.native.tsx index 60fc6ade0082..2459fd6c8553 100644 --- a/src/components/RoomNameInput/index.native.tsx +++ b/src/components/RoomNameInput/index.native.tsx @@ -9,7 +9,10 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import type RoomNameInputProps from './types'; -function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInputChange, ...props}: RoomNameInputProps, ref: ForwardedRef) { +function RoomNameInput( + {disabled = false, autoFocus = false, shouldDelayFocus = false, isFocused, value, onBlur, onChangeText, onInputChange, ...props}: RoomNameInputProps, + ref: ForwardedRef, +) { const {translate} = useLocalize(); /** @@ -34,6 +37,7 @@ function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInp // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} + disabled={disabled} label={translate('newRoomPage.roomName')} accessibilityLabel={translate('newRoomPage.roomName')} role={CONST.ROLE.PRESENTATION} @@ -43,6 +47,7 @@ function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInp maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} onBlur={(event) => isFocused && onBlur?.(event)} autoFocus={isFocused && autoFocus} + shouldDelayFocus={shouldDelayFocus} autoCapitalize="none" onChange={setModifiedRoomName} keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 diff --git a/src/components/RoomNameInput/index.tsx b/src/components/RoomNameInput/index.tsx index a6b8e4d83091..2606015116bb 100644 --- a/src/components/RoomNameInput/index.tsx +++ b/src/components/RoomNameInput/index.tsx @@ -8,7 +8,10 @@ import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/typ import CONST from '@src/CONST'; import type RoomNameInputProps from './types'; -function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInputChange, ...props}: RoomNameInputProps, ref: ForwardedRef) { +function RoomNameInput( + {disabled = false, autoFocus = false, shouldDelayFocus = false, isFocused, value, onBlur, onChangeText, onInputChange, ...props}: RoomNameInputProps, + ref: ForwardedRef, +) { const {translate} = useLocalize(); const [selection, setSelection] = useState<{start: number; end: number}>({start: 0, end: 0}); @@ -44,6 +47,7 @@ function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInp // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} + disabled={disabled} label={translate('newRoomPage.roomName')} accessibilityLabel={translate('newRoomPage.roomName')} role={CONST.ROLE.PRESENTATION} @@ -53,6 +57,7 @@ function RoomNameInput({isFocused, autoFocus, value, onBlur, onChangeText, onInp maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} onBlur={(event) => isFocused && onBlur?.(event)} autoFocus={isFocused && autoFocus} + shouldDelayFocus={shouldDelayFocus} autoCapitalize="none" onChange={setModifiedRoomName} onSelectionChange={(event) => setSelection(event.nativeEvent.selection)} From a7a74e0c4c98af97add0c978c6227c6c416250fd Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 19 Feb 2024 07:55:41 +0100 Subject: [PATCH 102/297] change file extension to .ts --- .../utils/{getCurrentBranchName.js => getCurrentBranchName.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/e2e/utils/{getCurrentBranchName.js => getCurrentBranchName.ts} (100%) diff --git a/tests/e2e/utils/getCurrentBranchName.js b/tests/e2e/utils/getCurrentBranchName.ts similarity index 100% rename from tests/e2e/utils/getCurrentBranchName.js rename to tests/e2e/utils/getCurrentBranchName.ts From 2d4d666d1317fc39cd82e28a407d4f1eb8906f55 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 19 Feb 2024 09:33:25 +0100 Subject: [PATCH 103/297] fix: add podfile --- ios/Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 80933065c450..e4f988173f40 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1434,7 +1434,7 @@ PODS: - React-Core - RNReactNativeHapticFeedback (2.2.0): - React-Core - - RNReanimated (3.6.1): + - RNReanimated (3.7.0): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1988,7 +1988,7 @@ SPEC CHECKSUMS: rnmapbox-maps: fcf7f1cbdc8bd7569c267d07284e8a5c7bee06ed RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 - RNReanimated: 57f436e7aa3d277fbfed05e003230b43428157c0 + RNReanimated: 7d6d32f238f914f13d9d6fb45c0aef557f7f901e RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a From 51f34ab996e756ef41e07702a37a8bf94e596b86 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 19 Feb 2024 10:34:19 +0100 Subject: [PATCH 104/297] feat: rename patches --- ... react-native-reanimated+3.7.0+001+fix-boost-dependency.patch} | 0 ...reanimated+3.6.1.patch => react-native-reanimated+3.7.0.patch} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename patches/{react-native-reanimated+3.6.1+001+fix-boost-dependency.patch => react-native-reanimated+3.7.0+001+fix-boost-dependency.patch} (100%) rename patches/{react-native-reanimated+3.6.1.patch => react-native-reanimated+3.7.0.patch} (100%) diff --git a/patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.7.0+001+fix-boost-dependency.patch similarity index 100% rename from patches/react-native-reanimated+3.6.1+001+fix-boost-dependency.patch rename to patches/react-native-reanimated+3.7.0+001+fix-boost-dependency.patch diff --git a/patches/react-native-reanimated+3.6.1.patch b/patches/react-native-reanimated+3.7.0.patch similarity index 100% rename from patches/react-native-reanimated+3.6.1.patch rename to patches/react-native-reanimated+3.7.0.patch From 2e8bcc800fd0243db08cc974a7f23011131e164a Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 19 Feb 2024 11:53:54 +0100 Subject: [PATCH 105/297] Make isFocused required prop and adjust ValidInputs comment --- src/components/Form/types.ts | 2 +- src/components/RoomNameInput/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 1e92f106b459..6ef1d41118f8 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -21,7 +21,7 @@ import type {BaseForm} from '@src/types/form/Form'; * when adding new inputs or removing old ones. * * TODO: Add remaining inputs here once these components are migrated to Typescript: - * EmojiPickerButtonDropdown | RoomNameInput | ValuePicker + * EmojiPickerButtonDropdown | ValuePicker */ type ValidInputs = | typeof TextInput diff --git a/src/components/RoomNameInput/types.ts b/src/components/RoomNameInput/types.ts index f90051de6fd0..80f08a01e472 100644 --- a/src/components/RoomNameInput/types.ts +++ b/src/components/RoomNameInput/types.ts @@ -13,7 +13,7 @@ type RoomNameInputProps = { onBlur?: (event: NativeSyntheticEvent) => void; autoFocus?: boolean; shouldDelayFocus?: boolean; - isFocused?: boolean; + isFocused: boolean; }; export default RoomNameInputProps; From 4de23c36c4b45d859d8de6a2e40d232c1d301123 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 20 Feb 2024 03:47:44 +0530 Subject: [PATCH 106/297] bump expensify-commons version --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c366a2ccdca3..4a21c9c248d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -31239,8 +31239,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", - "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", + "integrity": "sha512-nAe0fPbfRn/VYHe6mCp/APmMbda/NiHE3aZq7q0kWhPmz1LVTukeaREmZ7SN8auyLOy9/mS0RIQLeV0AR8vsrA==", "license": "MIT", "dependencies": { "classnames": "2.4.0", diff --git a/package.json b/package.json index 887210b5f9bf..9defcf24ffbd 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", From 1a91eaa68dac0cedd6c4cca2983a783f57c5b014 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 20 Feb 2024 03:59:47 +0530 Subject: [PATCH 107/297] fix: memoize task desciption default value --- src/pages/tasks/NewTaskDescriptionPage.js | 5 +++-- src/pages/tasks/NewTaskDetailsPage.js | 12 +++++++++--- src/pages/tasks/TaskDescriptionPage.js | 5 +++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index dbcb10d47f39..db02a99db067 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -1,6 +1,6 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -42,6 +42,7 @@ const parser = new ExpensiMark(); function NewTaskDescriptionPage(props) { const styles = useThemeStyles(); const {inputCallbackRef} = useAutoFocusInput(); + const defaultDescriptionValue = useMemo(() => parser.htmlToMarkdown(parser.replace(props.task.description)), [props.task.description]); const onSubmit = (values) => { Task.setDescriptionValue(values.taskDescription); @@ -85,7 +86,7 @@ function NewTaskDescriptionPage(props) { parser.htmlToMarkdown(parser.replace(taskDescription)), [taskDescription]); useEffect(() => { + if (!isFocused) { + return; + } setTaskTitle(props.task.title); setTaskDescription(parser.htmlToMarkdown(parser.replace(props.task.description || ''))); - }, [props.task]); + }, [isFocused, props.task.title, props.task.description]); /** * @param {Object} values - form input values passed by the Form component @@ -118,7 +124,7 @@ function NewTaskDetailsPage(props) { autoGrowHeight shouldSubmitForm containerStyles={[styles.autoGrowHeightMultilineInput]} - defaultValue={parser.htmlToMarkdown(parser.replace(taskDescription))} + defaultValue={defaultDescriptionValue} value={taskDescription} onValueChange={(value) => setTaskDescription(value)} /> diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index b8b48abd09ff..23a52384af6d 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -1,6 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {useCallback, useRef} from 'react'; +import React, {useCallback, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -80,6 +80,7 @@ function TaskDescriptionPage(props) { const isOpen = ReportUtils.isOpenTaskReport(props.report); const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen); + const defaultDescriptionValue = useMemo(() => parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || ''), [props.report]); useFocusEffect( useCallback(() => { @@ -121,7 +122,7 @@ function TaskDescriptionPage(props) { name={INPUT_IDS.DESCRIPTION} label={props.translate('newTaskPage.descriptionOptional')} accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} - defaultValue={parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || '')} + defaultValue={defaultDescriptionValue} ref={(el) => { if (!el) { return; From a4c949471bcebef0e4cb1495c93866f5e6b0d903 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 20 Feb 2024 15:19:10 +0800 Subject: [PATCH 108/297] disable tab only on start page --- src/pages/iou/request/IOURequestStartPage.js | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 3d80ab89347d..cb6ff07c819d 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -1,3 +1,4 @@ +import {useFocusEffect} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; @@ -88,18 +89,20 @@ function IOURequestStartPage({ [reportID], ); - useEffect(() => { - const handler = (event) => { - if (event.code !== CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) { - return; - } - event.preventDefault(); - event.stopPropagation(); - }; - KeyDownPressListener.addKeyDownPressListener(handler); - - return () => KeyDownPressListener.removeKeyDownPressListener(handler); - }, []); + useFocusEffect( + useCallback(() => { + const handler = (event) => { + if (event.code !== CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) { + return; + } + event.preventDefault(); + event.stopPropagation(); + }; + KeyDownPressListener.addKeyDownPressListener(handler); + + return () => KeyDownPressListener.removeKeyDownPressListener(handler); + }, []), + ); // Clear out the temporary money request if the reportID in the URL has changed from the transaction's reportID useEffect(() => { From 40366b7cf057fc5fd134ce3c7f1488e0f5cfa234 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 20 Feb 2024 08:58:50 +0100 Subject: [PATCH 109/297] Bump eslint and temporarly turn off new rules --- .eslintrc.js | 13 + package-lock.json | 964 +++++++--------------------------------------- package.json | 2 +- 3 files changed, 148 insertions(+), 831 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 281f8269804e..7ff933cea1ba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,6 +87,19 @@ module.exports = { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], plugins: ['react'], rules: { + 'no-promise-executor-return': 'off', + 'no-import-assign': 'off', + 'no-unsafe-optional-chaining': 'off', + 'react/jsx-no-useless-fragment': 'off', + 'prefer-regex-literals': 'off', + 'import/no-relative-packages': 'off', + 'react/no-unused-class-component-methods': 'off', + 'import/no-import-module-exports': 'off', + 'react/no-unstable-nested-components': 'off', + 'import/no-useless-path-segments': 'off', + 'react/sort-comp': 'off', + 'default-param-last': 'off', + 'rulesdir/no-multiple-onyx-in-file': 'off', 'rulesdir/onyx-props-must-have-default': 'off', 'react-native-a11y/has-accessibility-hint': ['off'], diff --git a/package-lock.json b/package-lock.json index dd9c7d39ddf9..1653d2d7dc15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -206,7 +206,7 @@ "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -450,6 +450,35 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/eslint-parser": { + "version": "7.23.10", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.10.tgz", + "integrity": "sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==", + "dev": true, + "peer": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", @@ -7324,20 +7353,71 @@ "license": "MIT" }, "node_modules/@lwc/eslint-plugin-lwc": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-0.11.0.tgz", - "integrity": "sha512-wJOD4XWOH91GaZfypMSKfEeMXqMfvKdsb2gSJ/9FEwJVlziKg1aagtRYJh2ln3DyEZV33tBC/p/dWzIeiwa1tg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@lwc/eslint-plugin-lwc/-/eslint-plugin-lwc-1.7.2.tgz", + "integrity": "sha512-fvdW/yvkNfqgt2Cc4EJCRYE55QJVNXdDaVTHRk5i1kkKP2Xj3GG0nAsYwXYqApEeRpUTpUZljPlO29/SWRXJoA==", "dev": true, - "license": "MIT", "dependencies": { - "minimatch": "^3.0.4" + "globals": "^13.24.0", + "minimatch": "^9.0.3" }, "engines": { "node": ">=10.0.0" }, "peerDependencies": { - "babel-eslint": "^10", - "eslint": "^6 || ^7" + "@babel/eslint-parser": "^7", + "eslint": "^7 || ^8" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@lwc/eslint-plugin-lwc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@malept/cross-spawn-promise": { @@ -7689,6 +7769,16 @@ "uuid": "8.3.2" } }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "peer": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -22852,24 +22942,6 @@ "node": ">=10" } }, - "node_modules/aria-query": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", - "integrity": "sha512-majUxHgLehQTeSA+hClx+DY09OVUqG3GtezWkF1krgLGNdlDu9l9V8DaqNMWbq4Eddc8wsyDA0hpDUtnYxQEXw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "ast-types-flow": "0.0.7", - "commander": "^2.11.0" - } - }, - "node_modules/aria-query/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -23330,13 +23402,6 @@ "node": ">=4" } }, - "node_modules/axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -25620,13 +25685,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" - }, "node_modules/charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", @@ -25967,16 +26025,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10" - } - }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -29485,6 +29533,27 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, "node_modules/eslint-config-airbnb-base": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", @@ -29529,579 +29598,24 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.43", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.43.tgz", - "integrity": "sha512-kLd6NyYbyb3mCB6VH6vu49/RllwNo0rdXcLUUGB7JGny+2N19jOmBJ4/GLKsbpFzvEZEghXfn7BITPRkxVJcgg==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.44.tgz", + "integrity": "sha512-fwa7lcQk7llYgqcWA1TX4kcSigYqSVkKGk+anODwYlYSbVbXwzzkQsncsaiWVTM7+eJdk46GmWPeiMAWOGWPvw==", "dev": true, "dependencies": { - "@lwc/eslint-plugin-lwc": "^0.11.0", + "@lwc/eslint-plugin-lwc": "^1.7.2", "babel-eslint": "^10.1.0", - "eslint": "6.8.0", - "eslint-config-airbnb": "18.0.1", - "eslint-config-airbnb-base": "14.0.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-airbnb-base": "15.0.0", "eslint-plugin-es": "^4.1.0", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.18.0", - "eslint-plugin-rulesdir": "^0.2.0", - "lodash": "^4.17.21", - "underscore": "^1.13.1" - } - }, - "node_modules/eslint-config-expensify/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint-config-expensify/node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/eslint-config-expensify/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/eslint-config-expensify/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-config-expensify/node_modules/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", - "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-config-airbnb-base": "^14.0.0", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2", "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^1.7.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-config-airbnb-base": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", - "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "confusing-browser-globals": "^1.0.7", - "object.assign": "^4.1.0", - "object.entries": "^1.1.0" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "eslint": "^5.16.0 || ^6.1.0", - "eslint-plugin-import": "^2.18.2" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-jsx-a11y": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", - "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.4.5", - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.2", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^7.0.2", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.0.tgz", - "integrity": "sha512-p+PGoGeV4SaZRDsXqdj9OWcOrOpZn8gXoGPcIQTzo2IDMbAKhNDnME9myZWqO3Ic4R3YmwAZ1lDjWl2R2hMUVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.1", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", - "prop-types": "^15.7.2", - "resolve": "^1.14.2" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react-hooks": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", - "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=7" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true, - "license": "ISC" - }, - "node_modules/eslint-config-expensify/node_modules/globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-config-expensify/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint-config-expensify/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-config-expensify/node_modules/jsx-ast-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", - "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.1", - "object.assign": "^4.1.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-config-expensify/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-config-expensify/node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint-config-expensify/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-config-expensify/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "eslint-plugin-react": "^7.18.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-rulesdir": "^0.2.2", + "lodash": "^4.17.21", + "underscore": "^1.13.6" } }, "node_modules/eslint-config-prettier": { @@ -30562,9 +30076,10 @@ } }, "node_modules/eslint-plugin-rulesdir": { - "version": "0.2.1", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.2.tgz", + "integrity": "sha512-qhBtmrWgehAIQeMDJ+Q+PnOz1DWUZMPeVrI0wE9NZtnpIMFUfh3aPKFYt2saeMSemZRrvUtjWfYwepsC8X+mjQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -31726,47 +31241,6 @@ "node": ">=0.10.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -32049,32 +31523,6 @@ "dev": true, "license": "ISC" }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -34649,107 +34097,6 @@ "css-in-js-utils": "^2.0.0" } }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -41644,13 +40991,6 @@ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -47387,16 +46727,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", @@ -53201,19 +52531,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/write-file-atomic": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", @@ -53225,19 +52542,6 @@ "signal-exit": "^3.0.2" } }, - "node_modules/write/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ws": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", diff --git a/package.json b/package.json index dfd0f1367de0..9a39dd6d96c8 100644 --- a/package.json +++ b/package.json @@ -254,7 +254,7 @@ "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.43", + "eslint-config-expensify": "^2.0.44", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", From 063131842d217fe466f2265769f8aac22c068768 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 20 Feb 2024 09:46:38 +0100 Subject: [PATCH 110/297] Fix eslint running on files in a build catalog --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 396bfd28c614..d3358a02fe4b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ **/node_modules/* **/dist/* +android/**/build/* .github/actions/**/index.js" docs/vendor/** From 3cc326ea3d38cc409768c5bc9acb06a18ac2fff5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 20 Feb 2024 09:58:48 +0100 Subject: [PATCH 111/297] Fix no-loss-of-precision --- tests/unit/NumberUtilsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/NumberUtilsTest.js b/tests/unit/NumberUtilsTest.js index 3fc547476be1..c363699118f1 100644 --- a/tests/unit/NumberUtilsTest.js +++ b/tests/unit/NumberUtilsTest.js @@ -5,7 +5,7 @@ describe('libs/NumberUtils', () => { const id = NumberUtils.rand64(); expect(typeof id).toBe('string'); // eslint-disable-next-line no-undef - expect(BigInt(id)).toBeLessThanOrEqual(BigInt(9223372036854775807)); + expect(BigInt(id)).toBeLessThanOrEqual(BigInt('9223372036854775807')); // eslint-disable-next-line no-undef expect(BigInt(id)).toBeGreaterThanOrEqual(0); }); From b105c6cdfb7531f0eb3c4e7c4743f305bac18b07 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 20 Feb 2024 10:07:54 +0100 Subject: [PATCH 112/297] Fix no-promise-executor-return --- .eslintrc.js | 1 - jest/setup.ts | 7 ++++++- .../QRShare/QRShareWithDownload/index.tsx | 3 ++- src/libs/BootSplash/index.ts | 4 +++- .../Environment/getEnvironment/index.native.ts | 9 ++++++--- .../LocalNotification/BrowserNotifications.ts | 6 ++++-- src/libs/Pusher/pusher.ts | 3 ++- src/libs/Request.ts | 4 +++- src/libs/RequestThrottle.ts | 5 +++-- src/libs/actions/Device/index.ts | 3 ++- src/libs/actions/Session/clearCache/index.ts | 5 ++++- src/libs/calculateAnchorPosition.ts | 8 +++++--- tests/e2e/server/index.js | 10 ++++++++-- tests/unit/APITest.js | 16 ++++++++++++---- tests/utils/TestHelper.js | 4 +++- 15 files changed, 63 insertions(+), 25 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 7ff933cea1ba..6d09dc089515 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,7 +87,6 @@ module.exports = { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], plugins: ['react'], rules: { - 'no-promise-executor-return': 'off', 'no-import-assign': 'off', 'no-unsafe-optional-chaining': 'off', 'react/jsx-no-useless-fragment': 'off', diff --git a/jest/setup.ts b/jest/setup.ts index 4a23a85edb83..11b0d77ed7ac 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -31,7 +31,12 @@ jest.spyOn(console, 'debug').mockImplementation((...params) => { // This mock is required for mocking file systems when running tests jest.mock('react-native-fs', () => ({ - unlink: jest.fn(() => new Promise((res) => res())), + unlink: jest.fn( + () => + new Promise((res) => { + res(); + }), + ), CachesDirectoryPath: jest.fn(), })); diff --git a/src/components/QRShare/QRShareWithDownload/index.tsx b/src/components/QRShare/QRShareWithDownload/index.tsx index 4a327e9c9249..7797f22a32d5 100644 --- a/src/components/QRShare/QRShareWithDownload/index.tsx +++ b/src/components/QRShare/QRShareWithDownload/index.tsx @@ -19,7 +19,8 @@ function QRShareWithDownload(props: QRShareProps, ref: ForwardedRef resolve(fileDownload(dataURL, getQrCodeFileName(props.title)))); diff --git a/src/libs/BootSplash/index.ts b/src/libs/BootSplash/index.ts index 644403d6abc9..774c5f7b06ac 100644 --- a/src/libs/BootSplash/index.ts +++ b/src/libs/BootSplash/index.ts @@ -2,7 +2,9 @@ import Log from '@libs/Log'; import type {VisibilityStatus} from './types'; function resolveAfter(delay: number): Promise { - return new Promise((resolve) => setTimeout(resolve, delay)); + return new Promise((resolve) => { + setTimeout(resolve, delay); + }); } function hide(): Promise { diff --git a/src/libs/Environment/getEnvironment/index.native.ts b/src/libs/Environment/getEnvironment/index.native.ts index fb3f70001aaa..6d298c3fdae9 100644 --- a/src/libs/Environment/getEnvironment/index.native.ts +++ b/src/libs/Environment/getEnvironment/index.native.ts @@ -12,17 +12,20 @@ function getEnvironment(): Promise { return new Promise((resolve) => { // If we've already set the environment, use the current value if (environment) { - return resolve(environment); + resolve(environment); + return; } if ((Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.DEV) { environment = CONST.ENVIRONMENT.DEV; - return resolve(environment); + resolve(environment); + return; } if ((Config?.ENVIRONMENT ?? CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.ADHOC) { environment = CONST.ENVIRONMENT.ADHOC; - return resolve(environment); + resolve(environment); + return; } // If we haven't set the environment yet and we aren't on dev/adhoc, check to see if this is a beta build diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index f44b6802b540..8908b91c4f42 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -18,14 +18,16 @@ function canUseBrowserNotifications(): Promise { return new Promise((resolve) => { // They have no browser notifications so we can't use this feature if (!window.Notification) { - return resolve(false); + resolve(false); + return; } // Check if they previously granted or denied us access to send a notification const permissionGranted = Notification.permission === 'granted'; if (permissionGranted || Notification.permission === 'denied') { - return resolve(permissionGranted); + resolve(permissionGranted); + return; } // Check their global preferences for browser notifications and ask permission if they have none diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts index b6a5390bd501..bc48111eadc5 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher.ts @@ -87,7 +87,8 @@ function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallba function init(args: Args, params?: unknown): Promise { return new Promise((resolve) => { if (socket) { - return resolve(); + resolve(); + return; } // Use this for debugging diff --git a/src/libs/Request.ts b/src/libs/Request.ts index aa94eccbca2e..fc31160bbc1c 100644 --- a/src/libs/Request.ts +++ b/src/libs/Request.ts @@ -13,7 +13,9 @@ function makeXHR(request: Request): Promise { // If we're using the Supportal token and this is not a Supportal request // let's just return a promise that will resolve itself. if (NetworkStore.getSupportAuthToken() && !NetworkStore.isSupportRequest(request.command)) { - return new Promise((resolve) => resolve()); + return new Promise((resolve) => { + resolve(); + }); } return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure); diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index 36935982afbb..4c524394cb2c 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -26,9 +26,10 @@ function sleep(): Promise { requestRetryCount++; return new Promise((resolve, reject) => { if (requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { - return setTimeout(resolve, getRequestWaitTime()); + setTimeout(resolve, getRequestWaitTime()); + return; } - return reject(); + reject(); }); } diff --git a/src/libs/actions/Device/index.ts b/src/libs/actions/Device/index.ts index 761e27d95a78..e7c19d20e4fe 100644 --- a/src/libs/actions/Device/index.ts +++ b/src/libs/actions/Device/index.ts @@ -12,7 +12,8 @@ let deviceID: string | null = null; function getDeviceID(): Promise { return new Promise((resolve) => { if (deviceID) { - return resolve(deviceID); + resolve(deviceID); + return; } const connectionID = Onyx.connect({ diff --git a/src/libs/actions/Session/clearCache/index.ts b/src/libs/actions/Session/clearCache/index.ts index 6d288c6cbd3b..3daa8ec2d7d7 100644 --- a/src/libs/actions/Session/clearCache/index.ts +++ b/src/libs/actions/Session/clearCache/index.ts @@ -1,5 +1,8 @@ import type ClearCache from './types'; -const clearStorage: ClearCache = () => new Promise((res) => res()); +const clearStorage: ClearCache = () => + new Promise((resolve) => { + resolve(); + }); export default clearStorage; diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 0f1e383522eb..3dc5924d023a 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -16,13 +16,15 @@ type AnchorOrigin = { export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { if (!anchorComponent) { - return resolve({horizontal: 0, vertical: 0}); + resolve({horizontal: 0, vertical: 0}); + return; } anchorComponent.measureInWindow((x, y, width, height) => { if (anchorOrigin?.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP && anchorOrigin?.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT) { - return resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)}); + resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)}); + return; } - return resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0)}); + resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0)}); }); }); } diff --git a/tests/e2e/server/index.js b/tests/e2e/server/index.js index 82152245d8e2..82d9f48e0269 100644 --- a/tests/e2e/server/index.js +++ b/tests/e2e/server/index.js @@ -191,8 +191,14 @@ const createServerInstance = () => { addTestStartedListener, addTestResultListener, addTestDoneListener, - start: () => new Promise((resolve) => server.listen(PORT, resolve)), - stop: () => new Promise((resolve) => server.close(resolve)), + start: () => + new Promise((resolve) => { + server.listen(PORT, resolve); + }), + stop: () => + new Promise((resolve) => { + server.close(resolve); + }), }; }; diff --git a/tests/unit/APITest.js b/tests/unit/APITest.js index 474ccbf36328..30c935c48571 100644 --- a/tests/unit/APITest.js +++ b/tests/unit/APITest.js @@ -167,7 +167,9 @@ describe('APITests', () => { expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); // We need to advance past the request throttle back off timer because the request won't be retried until then - return new Promise((resolve) => setTimeout(resolve, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS)).then(waitForBatchedUpdates); + return new Promise((resolve) => { + setTimeout(resolve, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); + }).then(waitForBatchedUpdates); }) .then(() => { // Finally, after it succeeds the queue should be empty @@ -217,7 +219,9 @@ describe('APITests', () => { expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})})]); // We let the SequentialQueue process again after its wait time - return new Promise((resolve) => setTimeout(resolve, RequestThrottle.getLastRequestWaitTime())); + return new Promise((resolve) => { + setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); + }); }) .then(() => { // Then we have retried the failing request @@ -228,7 +232,9 @@ describe('APITests', () => { expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})})]); // We let the SequentialQueue process again after its wait time - return new Promise((resolve) => setTimeout(resolve, RequestThrottle.getLastRequestWaitTime())).then(waitForBatchedUpdates); + return new Promise((resolve) => { + setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); + }).then(waitForBatchedUpdates); }) .then(() => { // Then the request is retried again @@ -534,7 +540,9 @@ describe('APITests', () => { expect(secondRequestCommandName).toBe('MockCommandThree'); // WHEN we advance the main queue timer and wait for promises - return new Promise((resolve) => setTimeout(resolve, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS)); + return new Promise((resolve) => { + setTimeout(resolve, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); + }); }) .then(() => { // THEN we should see that our third (non-persistable) request has run last diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 9059041afd19..b26c601a1c06 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -167,7 +167,9 @@ function getGlobalFetchMock() { if (!isPaused) { return Promise.resolve(getResponse()); } - return new Promise((resolve) => queue.push(resolve)); + return new Promise((resolve) => { + queue.push(resolve); + }); }); mockFetch.pause = () => (isPaused = true); From 472878f6c41b665fa0d2102dbfd76d4408221f21 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 20 Feb 2024 10:19:30 +0100 Subject: [PATCH 113/297] Turn off no-import-assign rule in tests --- .eslintrc.js | 1 - tests/.eslintrc.js | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 tests/.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js index 6d09dc089515..33d6489dfccd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,7 +87,6 @@ module.exports = { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], plugins: ['react'], rules: { - 'no-import-assign': 'off', 'no-unsafe-optional-chaining': 'off', 'react/jsx-no-useless-fragment': 'off', 'prefer-regex-literals': 'off', diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js new file mode 100644 index 000000000000..4972022ae5e5 --- /dev/null +++ b/tests/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-import-assign': 'off', + }, +}; From 9c54cb9e31807837567f8c89812207be39f49ea6 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 20 Feb 2024 10:32:05 +0100 Subject: [PATCH 114/297] Fix no-unsafe-optional-chaining --- .eslintrc.js | 1 - src/components/PopoverProvider/index.tsx | 2 +- src/libs/LoginUtils.ts | 2 +- .../Navigation/linkingConfig/customGetPathFromState.ts | 2 +- src/libs/Navigation/switchPolicyID.ts | 2 +- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Task.ts | 2 +- .../AboutPage/ShareLogList/BaseShareLogList.tsx | 4 ++-- src/pages/tasks/TaskAssigneeSelectorModal.js | 10 +++++++--- src/pages/tasks/TaskShareDestinationSelectorModal.js | 2 +- 11 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 33d6489dfccd..1016ae885308 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,7 +87,6 @@ module.exports = { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], plugins: ['react'], rules: { - 'no-unsafe-optional-chaining': 'off', 'react/jsx-no-useless-fragment': 'off', 'prefer-regex-literals': 'off', 'import/no-relative-packages': 'off', diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx index 69728d7be126..76fdd293261d 100644 --- a/src/components/PopoverProvider/index.tsx +++ b/src/components/PopoverProvider/index.tsx @@ -11,7 +11,7 @@ const PopoverContext = createContext({ }); function elementContains(ref: RefObject | undefined, target: EventTarget | null) { - if (ref?.current && 'contains' in ref?.current && ref?.current?.contains(target as Node)) { + if (ref?.current && 'contains' in ref.current && ref?.current?.contains(target as Node)) { return true; } return false; diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts index 3781890013eb..8ee6d94952f8 100644 --- a/src/libs/LoginUtils.ts +++ b/src/libs/LoginUtils.ts @@ -41,7 +41,7 @@ function validateNumber(values: string): string { const parsedPhoneNumber = parsePhoneNumber(values); if (parsedPhoneNumber.possible && Str.isValidPhone(values.slice(0))) { - return parsedPhoneNumber.number?.e164 + CONST.SMS.DOMAIN; + return `${parsedPhoneNumber.number?.e164}${CONST.SMS.DOMAIN}`; } return ''; diff --git a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts b/src/libs/Navigation/linkingConfig/customGetPathFromState.ts index 76ad0bb06bd0..4f7023d14db4 100644 --- a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts +++ b/src/libs/Navigation/linkingConfig/customGetPathFromState.ts @@ -8,7 +8,7 @@ import SCREENS from '@src/SCREENS'; const removePolicyIDParamFromState = (state: State) => { const stateCopy = _.cloneDeep(state); const bottomTabRoute = getTopmostBottomTabRoute(stateCopy); - if (bottomTabRoute?.name === SCREENS.HOME && bottomTabRoute?.params && 'policyID' in bottomTabRoute?.params) { + if (bottomTabRoute?.name === SCREENS.HOME && bottomTabRoute?.params && 'policyID' in bottomTabRoute.params) { delete bottomTabRoute.params.policyID; } return stateCopy; diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts index 72a7c3e32fb4..c425beca73fd 100644 --- a/src/libs/Navigation/switchPolicyID.ts +++ b/src/libs/Navigation/switchPolicyID.ts @@ -22,7 +22,7 @@ type ActionPayloadParams = { type CentralPaneRouteParams = Record & {policyID?: string; reportID?: string}; function checkIfActionPayloadNameIsEqual(action: Writable, screenName: string) { - return action?.payload && 'name' in action?.payload && action?.payload?.name === screenName; + return action?.payload && 'name' in action.payload && action?.payload?.name === screenName; } function getActionForBottomTabNavigator(action: StackNavigationAction, state: NavigationState, policyID?: string): Writable | undefined { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index d6f718da2b2c..f6bd83cd3045 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -262,7 +262,7 @@ Onyx.connect({ function addSMSDomainIfPhoneNumber(login: string): string { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(login); if (parsedPhoneNumber.possible && !Str.isValidEmail(login)) { - return parsedPhoneNumber.number?.e164 + CONST.SMS.DOMAIN; + return `${parsedPhoneNumber.number?.e164}${CONST.SMS.DOMAIN}`; } return login; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d0c3bf3e8c03..db3328c81c94 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1930,7 +1930,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

0, indexOffset, }); - indexOffset += searchOptions.recentReports?.length; + indexOffset += searchOptions.recentReports.length; sectionsList.push({ title: translate('common.contacts'), @@ -80,7 +80,7 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis shouldShow: searchOptions.personalDetails?.length > 0, indexOffset, }); - indexOffset += searchOptions.personalDetails?.length; + indexOffset += searchOptions.personalDetails.length; if (searchOptions.userToInvite) { sectionsList.push({ diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js index 14d2867aa1f4..bf453a9f0f6a 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.js @@ -76,7 +76,11 @@ function useOptions({reports}) { true, ); - const headerMessage = OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), debouncedSearchValue); + const headerMessage = OptionsListUtils.getHeaderMessage( + (recentReports.length || 0 + personalDetails.length || 0) !== 0 || currentUserOption, + Boolean(userToInvite), + debouncedSearchValue, + ); if (isLoading) { setIsLoading(false); @@ -138,7 +142,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { shouldShow: recentReports?.length > 0, indexOffset, }); - indexOffset += recentReports?.length; + indexOffset += recentReports?.length || 0; sectionsList.push({ title: translate('common.contacts'), @@ -146,7 +150,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { shouldShow: personalDetails?.length > 0, indexOffset, }); - indexOffset += personalDetails?.length; + indexOffset += personalDetails?.length || 0; if (userToInvite) { sectionsList.push({ diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index b8d9229e6158..ea22f388f404 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -98,7 +98,7 @@ function TaskShareDestinationSelectorModal(props) { shouldShow: true, indexOffset, }); - indexOffset += filteredRecentReports?.length; + indexOffset += filteredRecentReports.length; } return sections; From 5519dba50499dbf3e62ca3becbd0a616d876f450 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 20 Feb 2024 11:11:56 +0100 Subject: [PATCH 115/297] Fix react/jsx-no-useless-fragment --- .eslintrc.js | 1 - src/components/AddressSearch/index.tsx | 1 + .../AttachmentCarousel/index.native.js | 64 ++--- src/components/ConfirmedRoute.tsx | 36 ++- src/components/CustomDevMenu/index.native.tsx | 1 + src/components/CustomDevMenu/index.tsx | 1 + src/components/ExpensifyWordmark.tsx | 28 +-- .../HTMLRenderers/ImageRenderer.tsx | 2 +- src/components/MapView/MapView.tsx | 90 ++++--- src/components/MapView/MapView.website.tsx | 84 +++---- ...oraryForRefactorRequestConfirmationList.js | 2 +- src/components/MultipleAvatars.tsx | 237 +++++++++--------- src/components/OptionRow.tsx | 47 ++-- src/components/ReportWelcomeText.tsx | 112 ++++----- src/components/Section/index.tsx | 80 +++--- src/components/SelectionList/UserListItem.tsx | 41 ++- .../report/ReportActionItemParentAction.tsx | 54 ++-- .../reimburse/WorkspaceReimburseSection.js | 74 +++--- 18 files changed, 457 insertions(+), 498 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1016ae885308..102657937551 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,7 +87,6 @@ module.exports = { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], plugins: ['react'], rules: { - 'react/jsx-no-useless-fragment': 'off', 'prefer-regex-literals': 'off', 'import/no-relative-packages': 'off', 'react/no-unused-class-component-methods': 'off', diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 89e87eeebe54..58f44f059144 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -271,6 +271,7 @@ function AddressSearch( }; const renderHeaderComponent = () => ( + // eslint-disable-next-line react/jsx-no-useless-fragment <> {predefinedPlaces.length > 0 && ( <> diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 228f0d597a32..ca9352cdc18a 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -102,46 +102,48 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [setShouldShowArrows], ); + if (page == null) { + return ( + + ; + + ); + } + return ( - {page == null ? ( - + {page === -1 ? ( + ) : ( <> - {page === -1 ? ( - - ) : ( - <> - cycleThroughAttachments(-1)} - onForward={() => cycleThroughAttachments(1)} - autoHideArrow={autoHideArrows} - cancelAutoHideArrow={cancelAutoHideArrows} - /> - - updatePage(newPage)} - ref={pagerRef} - /> - - )} + cycleThroughAttachments(-1)} + onForward={() => cycleThroughAttachments(1)} + autoHideArrow={autoHideArrows} + cancelAutoHideArrow={cancelAutoHideArrows} + /> + updatePage(newPage)} + ref={pagerRef} + /> )} ); } + AttachmentCarousel.propTypes = propTypes; AttachmentCarousel.defaultProps = defaultProps; AttachmentCarousel.displayName = 'AttachmentCarousel'; diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index aaa43e33744d..5b54c751d8e0 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -82,26 +82,22 @@ function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) { return MapboxToken.stop; }, []); - return ( - <> - {!isOffline && Boolean(mapboxAccessToken?.token) ? ( - } - style={[styles.mapView, styles.br4]} - waypoints={waypointMarkers} - styleURL={CONST.MAPBOX.STYLE_URL} - /> - ) : ( - - )} - + return !isOffline && Boolean(mapboxAccessToken?.token) ? ( + } + style={[styles.mapView, styles.br4]} + waypoints={waypointMarkers} + styleURL={CONST.MAPBOX.STYLE_URL} + /> + ) : ( + ); } diff --git a/src/components/CustomDevMenu/index.native.tsx b/src/components/CustomDevMenu/index.native.tsx index 54f1336b4fef..968f97b9e91f 100644 --- a/src/components/CustomDevMenu/index.native.tsx +++ b/src/components/CustomDevMenu/index.native.tsx @@ -8,6 +8,7 @@ const CustomDevMenu: CustomDevMenuElement = Object.assign( useEffect(() => { DevMenu.addItem('Open Test Preferences', toggleTestToolsModal); }, []); + // eslint-disable-next-line react/jsx-no-useless-fragment return <>; }, { diff --git a/src/components/CustomDevMenu/index.tsx b/src/components/CustomDevMenu/index.tsx index 4306d0cae090..6c33f2868b9d 100644 --- a/src/components/CustomDevMenu/index.tsx +++ b/src/components/CustomDevMenu/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type CustomDevMenuElement from './types'; +// eslint-disable-next-line react/jsx-no-useless-fragment const CustomDevMenu: CustomDevMenuElement = Object.assign(() => <>, {displayName: 'CustomDevMenu'}); export default CustomDevMenu; diff --git a/src/components/ExpensifyWordmark.tsx b/src/components/ExpensifyWordmark.tsx index 0e8f78686b07..3d340da84f8b 100644 --- a/src/components/ExpensifyWordmark.tsx +++ b/src/components/ExpensifyWordmark.tsx @@ -34,21 +34,19 @@ function ExpensifyWordmark({isSmallScreenWidth, style}: ExpensifyWordmarkProps) const LogoComponent = logoComponents[environment]; return ( - <> - - - - + + + ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index fd2d80c4d79a..b07f366e3382 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -74,7 +74,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { ); return imagePreviewModalDisabled ? ( - <>{thumbnailImageComponent} + thumbnailImageComponent ) : ( {({anchor, report, action, checkIfContextMenuActive}) => ( diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index a3178f642852..4ee1e83eab9c 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -140,54 +140,50 @@ const MapView = forwardRef( } }; - return ( - <> - {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( - - setUserInteractedWithMap(true)} - pitchEnabled={pitchEnabled} - attributionPosition={{...styles.r2, ...styles.b2}} - scaleBarEnabled={false} - logoPosition={{...styles.l2, ...styles.b2}} - // eslint-disable-next-line react/jsx-props-no-spreading - {...responder.panHandlers} - > - - - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - - {directionCoordinates && } - - - ) : ( - + setUserInteractedWithMap(true)} + pitchEnabled={pitchEnabled} + attributionPosition={{...styles.r2, ...styles.b2}} + scaleBarEnabled={false} + logoPosition={{...styles.l2, ...styles.b2}} + // eslint-disable-next-line react/jsx-props-no-spreading + {...responder.panHandlers} + > + - )} - + + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + + {directionCoordinates && } + + + ) : ( + ); }, ); diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 289f7d0d62a8..5efd4e734017 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -173,50 +173,46 @@ const MapView = forwardRef( [mapRef], ); - return ( - <> - {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( - - setUserInteractedWithMap(true)} - ref={setRef} - mapLib={mapboxgl} - mapboxAccessToken={accessToken} - initialViewState={{ - longitude: currentPosition?.longitude, - latitude: currentPosition?.latitude, - zoom: initialState.zoom, - }} - style={StyleUtils.getTextColorStyle(theme.mapAttributionText)} - mapStyle={styleURL} - > - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - - ) : ( - - )} - + return !isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( + + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: currentPosition?.longitude, + latitude: currentPosition?.latitude, + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(theme.mapAttributionText)} + mapStyle={styleURL} + > + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + + ) : ( + ); }, ); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 3939e847707d..c4805596c1f2 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -904,7 +904,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} - {shouldShowAllFields && <>{supplementaryFields}} + {shouldShowAllFields && supplementaryFields} ); } diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 1d1eea0d20ba..41a1ea6864db 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -173,145 +173,134 @@ function MultipleAvatars({ avatarContainerStyles = StyleUtils.combineStyles([styles.alignItemsCenter, styles.flexRow, StyleUtils.getHeight(height)]); } - return ( - <> - {shouldStackHorizontally ? ( - avatarRows.map((avatars, rowIndex) => ( - ( + + {[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => ( + - {[...avatars].splice(0, maxAvatarsInRow).map((icon, index) => ( - - - - - - ))} - {avatars.length > maxAvatarsInRow && ( - - - - {`+${avatars.length - maxAvatarsInRow}`} - - - - )} + + + + + ))} + {avatars.length > maxAvatarsInRow && ( + + + + {`+${avatars.length - maxAvatarsInRow}`} + + + + )} + + )) + ) : ( + + + + {/* View is necessary for tooltip to show for multiple avatars in LHN */} + + - )) - ) : ( - - + + + {icons.length === 2 ? ( - {/* View is necessary for tooltip to show for multiple avatars in LHN */} - - {icons.length === 2 ? ( - + + - - - - - ) : ( - - - - {`+${icons.length - 1}`} - - - - )} - - + {`+${icons.length - 1}`} + + + + )} - )} - + + ); } diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 93c744225237..7b45fd963fe7 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -262,32 +262,29 @@ function OptionRow({ /> )} - {showSelectedState && ( - <> - {shouldShowSelectedStateAsButton && !isSelected ? ( -