From be682af3bd4311fcf6a82eb7b44b8d565655937b Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 26 Jan 2024 12:07:04 +0000 Subject: [PATCH 001/173] refactor(typescript): migrate iou page --- src/ONYXKEYS.ts | 1 + src/libs/Navigation/types.ts | 7 +- src/libs/actions/IOU.js | 1 - .../withReportAndReportActionOrNotFound.tsx | 22 +- src/pages/iou/NewDistanceRequestPage.js | 85 -------- src/pages/iou/NewDistanceRequestPage.tsx | 61 ++++++ src/pages/iou/SplitBillDetailsPage.js | 194 ------------------ src/pages/iou/SplitBillDetailsPage.tsx | 170 +++++++++++++++ ...AmountPage.js => NewRequestAmountPage.tsx} | 97 ++++----- 9 files changed, 294 insertions(+), 344 deletions(-) delete mode 100644 src/pages/iou/NewDistanceRequestPage.js create mode 100644 src/pages/iou/NewDistanceRequestPage.tsx delete mode 100644 src/pages/iou/SplitBillDetailsPage.js create mode 100644 src/pages/iou/SplitBillDetailsPage.tsx rename src/pages/iou/steps/{NewRequestAmountPage.js => NewRequestAmountPage.tsx} (66%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9693c907a5fe..5906328b889c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -473,6 +473,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; + [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolation[]; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index dd5a7720f00d..cf0582070747 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -180,7 +180,11 @@ type RoomInviteNavigatorParamList = { type MoneyRequestNavigatorParamList = { [SCREENS.MONEY_REQUEST.ROOT]: undefined; - [SCREENS.MONEY_REQUEST.AMOUNT]: undefined; + [SCREENS.MONEY_REQUEST.AMOUNT]: { + iouType: string; + reportID: string; + currency: string; + }; [SCREENS.MONEY_REQUEST.PARTICIPANTS]: { iouType: string; reportID: string; @@ -282,6 +286,7 @@ type EnablePaymentsNavigatorParamList = { type SplitDetailsNavigatorParamList = { [SCREENS.SPLIT_DETAILS.ROOT]: { + reportID: string; reportActionID: string; }; [SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: undefined; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index d258b5419103..6f9fb9945f65 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3626,7 +3626,6 @@ function setUpDistanceTransaction() { * @param {Object} iou * @param {String} iouType * @param {Object} report - * @param {String} report.reportID * @param {String} path */ function navigateToNextPage(iou, iouType, report, path = '') { diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index ed686852158b..b3d1f3e5ae09 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -1,5 +1,6 @@ /* eslint-disable rulesdir/no-negated-variables */ import type {RouteProp} from '@react-navigation/native'; +import {StackScreenProps} from '@react-navigation/stack'; import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {useCallback, useEffect} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -37,12 +38,21 @@ type OnyxProps = { isLoadingReportData: OnyxEntry; }; -type ComponentProps = OnyxProps & - WindowDimensionsProps & { - route: RouteProp<{params: {reportID: string; reportActionID: string}}>; - }; +type WithReportAndReportActionOrNotFound = OnyxProps & + WindowDimensionsProps & + StackScreenProps< + Record< + string, + { + reportID: string; + reportActionID: string; + } + > + >; -export default function (WrappedComponent: ComponentType>): ComponentType> { +export default function ( + WrappedComponent: ComponentType>, +): ComponentType> { function WithReportOrNotFound(props: TProps, ref: ForwardedRef) { const getReportAction = useCallback(() => { let reportAction: OnyxTypes.ReportAction | Record | undefined = props.reportActions?.[`${props.route.params.reportActionID}`]; @@ -118,3 +128,5 @@ export default function (WrappedComponent: withWindowDimensions, )(React.forwardRef(WithReportOrNotFound)); } + +export type {WithReportAndReportActionOrNotFound}; diff --git a/src/pages/iou/NewDistanceRequestPage.js b/src/pages/iou/NewDistanceRequestPage.js deleted file mode 100644 index 750ac5d0141e..000000000000 --- a/src/pages/iou/NewDistanceRequestPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import DistanceRequest from '@components/DistanceRequest'; -import Navigation from '@libs/Navigation/Navigation'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import {iouPropTypes} from './propTypes'; - -const propTypes = { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The report on which the request is initiated on */ - report: reportPropTypes, - - /** Passed from the navigator */ - route: PropTypes.shape({ - /** Parameters the route gets */ - params: PropTypes.shape({ - /** Type of IOU */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)), - /** Id of the report on which the distance request is being created */ - reportID: PropTypes.string, - }), - }), -}; - -const defaultProps = { - iou: {}, - report: {}, - route: { - params: { - iouType: '', - reportID: '', - }, - }, -}; - -// This component is responsible for getting the transactionID from the IOU key, or creating the transaction if it doesn't exist yet, and then passing the transactionID. -// You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that DistanceRequest can subscribe to the transaction. -function NewDistanceRequestPage({iou, report, route}) { - const iouType = lodashGet(route, 'params.iouType', 'request'); - const isEditingNewRequest = Navigation.getActiveRoute().includes('address'); - - useEffect(() => { - if (iou.transactionID) { - return; - } - IOU.setUpDistanceTransaction(); - }, [iou.transactionID]); - - const onSubmit = useCallback(() => { - if (isEditingNewRequest) { - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report.reportID)); - return; - } - IOU.navigateToNextPage(iou, iouType, report); - }, [iou, iouType, isEditingNewRequest, report]); - - return ( - - ); -} - -NewDistanceRequestPage.displayName = 'NewDistanceRequestPage'; -NewDistanceRequestPage.propTypes = propTypes; -NewDistanceRequestPage.defaultProps = defaultProps; -export default withOnyx({ - iou: {key: ONYXKEYS.IOU}, - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID')}`, - }, -})(NewDistanceRequestPage); diff --git a/src/pages/iou/NewDistanceRequestPage.tsx b/src/pages/iou/NewDistanceRequestPage.tsx new file mode 100644 index 000000000000..f40eb5d4fbe1 --- /dev/null +++ b/src/pages/iou/NewDistanceRequestPage.tsx @@ -0,0 +1,61 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useEffect} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import DistanceRequest from '@components/DistanceRequest'; +import Navigation from '@libs/Navigation/Navigation'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import * as IOU from '@userActions/IOU'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {IOU as IOUType, Report} from '@src/types/onyx'; + +type NewDistanceRequestPageOnyxProps = { + iou: OnyxEntry; + report: OnyxEntry; +}; + +type NewDistanceRequestPageProps = NewDistanceRequestPageOnyxProps & StackScreenProps; + +// This component is responsible for getting the transactionID from the IOU key, or creating the transaction if it doesn't exist yet, and then passing the transactionID. +// You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that DistanceRequest can subscribe to the transaction. +function NewDistanceRequestPage({iou, report, route}: NewDistanceRequestPageProps) { + const iouType = route.params.iouType ?? 'request'; + const isEditingNewRequest = Navigation.getActiveRoute().includes('address'); + + useEffect(() => { + if (iou?.transactionID) { + return; + } + IOU.setUpDistanceTransaction(); + }, [iou?.transactionID]); + + const onSubmit = useCallback(() => { + if (isEditingNewRequest) { + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report?.reportID)); + return; + } + IOU.navigateToNextPage(iou ?? {}, iouType, report ?? {}); + }, [iou, iouType, isEditingNewRequest, report]); + + return ( + + ); +} + +NewDistanceRequestPage.displayName = 'NewDistanceRequestPage'; + +export default withOnyx({ + iou: {key: ONYXKEYS.IOU}, + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, + }, +})(NewDistanceRequestPage); diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js deleted file mode 100644 index 94c2f1c31242..000000000000 --- a/src/pages/iou/SplitBillDetailsPage.js +++ /dev/null @@ -1,194 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; -import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar'; -import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /* Onyx Props */ - - /** The personal details of the person who is logged in */ - personalDetails: personalDetailsPropType, - - /** The active report */ - report: reportPropTypes.isRequired, - - /** Array of report actions for this report */ - reportActions: PropTypes.shape(reportActionPropTypes), - - /** The current transaction */ - transaction: transactionPropTypes.isRequired, - - /** The draft transaction that holds data to be persisited on the current transaction */ - draftTransaction: transactionPropTypes, - - /** Route params */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** Report ID passed via route r/:reportID/split/details */ - reportID: PropTypes.string, - - /** ReportActionID passed via route r/split/:reportActionID */ - reportActionID: PropTypes.string, - }), - }).isRequired, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - - /** Currently logged in user email */ - email: PropTypes.string, - }).isRequired, -}; - -const defaultProps = { - personalDetails: {}, - reportActions: {}, - draftTransaction: undefined, -}; - -function SplitBillDetailsPage(props) { - const styles = useThemeStyles(); - const {reportID} = props.report; - const {translate} = useLocalize(); - const reportAction = props.reportActions[`${props.route.params.reportActionID.toString()}`]; - const participantAccountIDs = reportAction.originalMessage.participantAccountIDs; - - // In case this is workspace split bill, we manually add the workspace as the second participant of the split bill - // because we don't save any accountID in the report action's originalMessage other than the payee's accountID - let participants; - if (ReportUtils.isPolicyExpenseChat(props.report)) { - participants = [ - OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true}, props.personalDetails), - OptionsListUtils.getPolicyExpenseReportOption({...props.report, selected: true}), - ]; - } else { - participants = _.map(participantAccountIDs, (accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true}, props.personalDetails)); - } - const payeePersonalDetails = props.personalDetails[reportAction.actorAccountID]; - const participantsExcludingPayee = _.filter(participants, (participant) => participant.accountID !== reportAction.actorAccountID); - - const isScanning = TransactionUtils.hasReceipt(props.transaction) && TransactionUtils.isReceiptBeingScanned(props.transaction); - const hasSmartScanFailed = TransactionUtils.hasReceipt(props.transaction) && props.transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANFAILED; - const isEditingSplitBill = props.session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(props.transaction); - - const { - amount: splitAmount, - currency: splitCurrency, - comment: splitComment, - merchant: splitMerchant, - created: splitCreated, - category: splitCategory, - tag: splitTag, - } = isEditingSplitBill && props.draftTransaction ? ReportUtils.getTransactionDetails(props.draftTransaction) : ReportUtils.getTransactionDetails(props.transaction); - - const onConfirm = useCallback( - () => IOU.completeSplitBill(reportID, reportAction, props.draftTransaction, props.session.accountID, props.session.email), - [reportID, reportAction, props.draftTransaction, props.session.accountID, props.session.email], - ); - - return ( - - - - - {isScanning && ( - - )} - {Boolean(participants.length) && ( - - )} - - - - ); -} - -SplitBillDetailsPage.propTypes = propTypes; -SplitBillDetailsPage.defaultProps = defaultProps; -SplitBillDetailsPage.displayName = 'SplitBillDetailsPage'; - -export default compose( - withReportAndReportActionOrNotFound, - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - reportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, - canEvict: false, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({route, reportActions}) => { - const reportAction = reportActions[`${route.params.reportActionID.toString()}`]; - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - draftTransaction: { - key: ({route, reportActions}) => { - const reportAction = reportActions[`${route.params.reportActionID.toString()}`]; - return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), -)(SplitBillDetailsPage); diff --git a/src/pages/iou/SplitBillDetailsPage.tsx b/src/pages/iou/SplitBillDetailsPage.tsx new file mode 100644 index 000000000000..742f6360e45b --- /dev/null +++ b/src/pages/iou/SplitBillDetailsPage.tsx @@ -0,0 +1,170 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; +import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SplitDetailsNavigatorParamList} from '@libs/Navigation/types'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound'; +import type {WithReportAndReportActionOrNotFound} from '@pages/home/report/withReportAndReportActionOrNotFound'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList, Report, Session, Transaction} from '@src/types/onyx'; +import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import type {ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type SplitBillDetailsPageOnyxProps = { + /** The personal details of the person who is logged in */ + personalDetails: OnyxEntry; + + /** The active report */ + report: OnyxEntry; + + /** Array of report actions for this report */ + reportActions: OnyxEntry; + + /** The current transaction */ + transaction: OnyxEntry; + + /** The draft transaction that holds data to be persisited on the current transaction */ + draftTransaction: OnyxEntry; + + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type SplitBillDetailsPageProps = WithReportAndReportActionOrNotFound & SplitBillDetailsPageOnyxProps & StackScreenProps; + +function SplitBillDetailsPage({personalDetails, report, route, reportActions, transaction, draftTransaction, session}: SplitBillDetailsPageProps) { + const styles = useThemeStyles(); + const {reportID} = report ?? {}; + const {translate} = useLocalize(); + const reportAction = reportActions?.[route.params.reportActionID] as (ReportActionBase & OriginalMessageIOU) | undefined; + const participantAccountIDs = reportAction?.originalMessage.participantAccountIDs ?? []; + + // In case this is workspace split bill, we manually add the workspace as the second participant of the split bill + // because we don't save any accountID in the report action's originalMessage other than the payee's accountID + let participants; + if (ReportUtils.isPolicyExpenseChat(report)) { + participants = [ + // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. + OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true}, personalDetails), + OptionsListUtils.getPolicyExpenseReportOption({...report, selected: true}), + ]; + } else { + // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. + participants = participantAccountIDs.map((accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true}, personalDetails)); + } + const payeePersonalDetails = personalDetails?.[reportAction?.actorAccountID ?? 0]; + // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. + const participantsExcludingPayee = participants.filter((participant) => participant.accountID !== reportAction?.actorAccountID); + + const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + const hasSmartScanFailed = TransactionUtils.hasReceipt(transaction) && transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCANFAILED; + const isEditingSplitBill = session?.accountID === reportAction?.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction ?? ({} as Transaction)); + + const { + amount: splitAmount, + currency: splitCurrency, + comment: splitComment, + merchant: splitMerchant, + created: splitCreated, + category: splitCategory, + tag: splitTag, + } = ReportUtils.getTransactionDetails(isEditingSplitBill && draftTransaction ? draftTransaction : transaction) ?? {}; + + const onConfirm = useCallback( + () => IOU.completeSplitBill(Number(reportID), reportAction ?? {}, draftTransaction ?? {}, session?.accountID ?? -1, session?.email ?? ''), + [reportID, reportAction, draftTransaction, session?.accountID, session?.email], + ); + + return ( + + + + + {isScanning && ( + + )} + {Boolean(participants.length) && ( + + )} + + + + ); +} + +SplitBillDetailsPage.displayName = 'SplitBillDetailsPage'; + +const WrappedComponent = withOnyx>({ + transaction: { + key: ({route, reportActions}) => { + const reportAction = reportActions?.[route.params.reportActionID] as (ReportActionBase & OriginalMessageIOU) | undefined; + return `${ONYXKEYS.COLLECTION.TRANSACTION}${reportAction?.originalMessage.IOUTransactionID ?? 0}`; + }, + }, + draftTransaction: { + key: ({route, reportActions}) => { + const reportAction = reportActions?.[route.params.reportActionID] as (ReportActionBase & OriginalMessageIOU) | undefined; + return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${reportAction?.originalMessage.IOUTransactionID ?? 0}`; + }, + }, +})(withReportAndReportActionOrNotFound(SplitBillDetailsPage)); + +export default withOnyx, Omit>({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, + }, + reportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, + canEvict: false, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + session: { + key: ONYXKEYS.SESSION, + }, +})(WrappedComponent); diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.tsx similarity index 66% rename from src/pages/iou/steps/NewRequestAmountPage.js rename to src/pages/iou/steps/NewRequestAmountPage.tsx index 1df74569e4c3..4d530c050c22 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.js +++ b/src/pages/iou/steps/NewRequestAmountPage.tsx @@ -1,10 +1,11 @@ import {useFocusEffect} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; +import type {TextInput as RNTextInput} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -14,66 +15,45 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as IOUUtils from '@libs/IOUUtils'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {IOU as IOUType, Report} from '@src/types/onyx'; import MoneyRequestAmountForm from './MoneyRequestAmountForm'; -const propTypes = { - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - - /** Selected currency from IOUCurrencySelection */ - currency: PropTypes.string, - }), - }).isRequired, - - /** The report on which the request is initiated on */ - report: reportPropTypes, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ - selectedTab: PropTypes.oneOf(_.values(CONST.TAB_REQUEST)), +type NewRequestAmountPageOnyxProps = { + iou: OnyxEntry; + report: OnyxEntry; + selectedTab: OnyxEntry; }; -const defaultProps = { - report: {}, - iou: iouDefaultProps, - selectedTab: CONST.TAB_REQUEST.MANUAL, -}; +type NewRequestAmountPageProps = NewRequestAmountPageOnyxProps & StackScreenProps; -function NewRequestAmountPage({route, iou, report, selectedTab}) { +function NewRequestAmountPage({route, iou, report, selectedTab}: NewRequestAmountPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const prevMoneyRequestID = useRef(iou.id); - const textInput = useRef(null); + const prevMoneyRequestID = useRef(iou?.id); + const textInput = useRef(null); - const iouType = lodashGet(route, 'params.iouType', ''); - const reportID = lodashGet(route, 'params.reportID', ''); + const iouType = route.params.iouType ?? ''; + const reportID = route.params.reportID ?? ''; const isEditing = Navigation.getActiveRoute().includes('amount'); - const currentCurrency = lodashGet(route, 'params.currency', ''); - const isDistanceRequestTab = MoneyRequestUtils.isDistanceRequest(iouType, selectedTab); + const currentCurrency = route.params.currency ?? ''; + const isDistanceRequestTab = MoneyRequestUtils.isDistanceRequest(iouType as ValueOf, selectedTab as ValueOf); - const currency = CurrencyUtils.isValidCurrencyCode(currentCurrency) ? currentCurrency : iou.currency; + const currency = CurrencyUtils.isValidCurrencyCode(currentCurrency) ? currentCurrency : iou?.currency ?? ''; - const focusTimeoutRef = useRef(null); + const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { - focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); + focusTimeoutRef.current = setTimeout(() => { + textInput.current?.focus(); + }, CONST.ANIMATED_TRANSITION); return () => { if (!focusTimeoutRef.current) { return; @@ -88,29 +68,29 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { useEffect(() => { if (isEditing) { // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request - if (prevMoneyRequestID.current !== iou.id) { + if (prevMoneyRequestID.current !== iou?.id) { // The ID is cleared on completing a request. In that case, we will do nothing. - if (!iou.id) { + if (!iou?.id) { return; } Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); return; } const moneyRequestID = `${iouType}${reportID}`; - const shouldReset = iou.id !== moneyRequestID; + const shouldReset = iou?.id !== moneyRequestID; if (shouldReset) { IOU.resetMoneyRequestInfo(moneyRequestID); } - if (!isDistanceRequestTab && (_.isEmpty(iou.participants) || iou.amount === 0 || shouldReset)) { + if (!isDistanceRequestTab && (!iou?.participants?.length || iou?.amount === 0 || shouldReset)) { Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); } } return () => { - prevMoneyRequestID.current = iou.id; + prevMoneyRequestID.current = iou?.id; }; - }, [iou.participants, iou.amount, iou.id, isEditing, iouType, reportID, isDistanceRequestTab]); + }, [isEditing, iouType, reportID, isDistanceRequestTab, iou]); const navigateBack = () => { Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); @@ -128,7 +108,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { Navigation.navigate(ROUTES.MONEY_REQUEST_CURRENCY.getRoute(iouType, reportID, currency, activeRoute)); }; - const navigateToNextPage = ({amount}) => { + const navigateToNextPage = ({amount}: {amount: string}) => { const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); IOU.setMoneyRequestAmount(amountInSmallestCurrencyUnits); IOU.setMoneyRequestCurrency(currency); @@ -138,15 +118,18 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { return; } - IOU.navigateToNextPage(iou, iouType, report); + IOU.navigateToNextPage(iou ?? {}, iouType, report ?? {}); }; const content = ( (textInput.current = e)} + amount={iou?.amount} + ref={(e) => { + textInput.current = e; + }} onCurrencyButtonPress={navigateToCurrencySelectionPage} onSubmitButtonPress={navigateToNextPage} selectedTab={selectedTab} @@ -180,14 +163,12 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { ); } -NewRequestAmountPage.propTypes = propTypes; -NewRequestAmountPage.defaultProps = defaultProps; NewRequestAmountPage.displayName = 'NewRequestAmountPage'; -export default withOnyx({ +export default withOnyx({ iou: {key: ONYXKEYS.IOU}, report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`, + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID ?? ''}`, }, selectedTab: { key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, From 53ada16d87aa815b3901029b36b6953741b7450a Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 6 Feb 2024 20:40:36 -0800 Subject: [PATCH 002/173] report action utils functions --- src/libs/ReportActionsUtils.ts | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1aeb6e6e7343..6f355137265d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -492,6 +492,22 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null, s return getSortedReportActions(baseURLAdjustedReportActions, true, shouldMarkTheFirstItemAsNewest); } +/** + * This method returns a combined array of report actions from a parent report and child transaction thread report that + * are ready for display in the ReportActionView. + */ +function getCombinedReportActionsForDisplay(reportActions: ReportAction[], transactionThreadReportActions: ReportAction[]): ReportAction[] { + + // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action + const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter(action => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); + + // Sort the combined list of parent report actions and transaction thread report actions + const sortedReportActions = getSortedReportActions([...reportActions, ...filteredTransactionThreadReportActions], true); + + // Filter out IOU report actions because we don't want to show any preview actions for one transaction reports + return sortedReportActions.filter(action => action.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU); +} + /** * In some cases, there can be multiple closed report actions in a chat report. * This method returns the last closed report action so we can always show the correct archived report reason. @@ -660,6 +676,24 @@ function getAllReportActions(reportID: string): ReportActions { return allReportActions?.[reportID] ?? {}; } +/** + * Gets an array of IOU report actions + */ +function getIOUReportActions(reportID: string): ReportAction[] | null { + const reportActions = Object.values(getAllReportActions(reportID)); + if (!reportActions.length) { + return null; + } + + const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT]; + const iouRequestActions = reportActions?.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && iouRequestTypes.includes(action.originalMessage.type)) ?? []; + + if (!iouRequestActions.length) { + return null; + } + return iouRequestActions; +} + /** * Check whether a report action is an attachment (a file, such as an image or a zip). * @@ -829,6 +863,7 @@ function isCurrentActionUnread(report: Report | EmptyObject, reportAction: Repor export { extractLinksFromMessageHtml, getAllReportActions, + getIOUReportActions, getIOUReportIDFromReportActionPreview, getLastClosedReportAction, getLastVisibleAction, @@ -843,6 +878,7 @@ export { getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, + getCombinedReportActionsForDisplay, isConsecutiveActionMadeByPreviousActor, isCreatedAction, isCreatedTaskReportAction, From df1b56c47b65defc8c439c9c72c9fe0962a5263e Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 6 Feb 2024 20:41:01 -0800 Subject: [PATCH 003/173] report utils functions for one transaction report --- src/libs/ReportUtils.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5c6672d14bd7..9107322f0c67 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1166,6 +1166,38 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { return isIOUReport(report) || isExpenseReport(report); } +/** + * Checks if a report has only one transaction associated with it + */ +function isOneTransactionReport(reportOrID: OnyxEntry | string): boolean { + const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; + + // Check the parent report (which would be the IOU or expense report if the passed report is an IOU or expense request) + // to see how many IOU report actions it contains + const iouReportActions = ReportActionsUtils.getIOUReportActions(report?.reportID ?? ''); + return (iouReportActions?.length ?? 0) === 1; +} + +/** + * Returns the reportID of the first transaction thread associated with a report + */ +function getOneTransactionThreadReportID(reportOrID: OnyxEntry | string): string | undefined { + const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; + + // Get all IOU report actions for the report. + const iouReportAction = ReportActionsUtils.getIOUReportActions(report?.reportID ?? '')?.find(reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.childReportID); + return String(iouReportAction?.childReportID) ?? '0'; +} + +/** + * Checks if a report is a transaction thread associated with a report that has only one transaction + */ +function isOneTransactionThread(reportOrID: OnyxEntry | string): boolean { + const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; + const parentReport = getParentReport(report); + return isOneTransactionReport(parentReport?.reportID ?? ''); +} + /** * Should return true only for personal 1:1 report * @@ -4907,6 +4939,9 @@ export { hasSingleParticipant, getReportRecipientAccountIDs, isOneOnOneChat, + isOneTransactionReport, + getOneTransactionThreadReportID, + isOneTransactionThread, goBackToDetailsPage, getTransactionReportName, getTransactionDetails, From b841b6aebd771e963e80d9707c3f8d9e77bef45a Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 6 Feb 2024 20:41:54 -0800 Subject: [PATCH 004/173] hide background for money requests on a single transaction report --- src/components/ReportActionItem/MoneyRequestView.tsx | 10 +++++++--- src/pages/home/report/ReportActionItem.js | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 3a3aef6cabcd..c0d15fb4dc02 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -68,6 +68,9 @@ type MoneyRequestViewPropsWithoutTransaction = MoneyRequestViewOnyxPropsWithoutT /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; + + /** Whether we should display the animated above the component */ + shouldShowAnimatedBackground: boolean; }; type MoneyRequestViewProps = MoneyRequestViewTransactionOnyxProps & MoneyRequestViewPropsWithoutTransaction; @@ -82,6 +85,7 @@ function MoneyRequestView({ policyTags, policy, transactionViolations, + shouldShowAnimatedBackground, }: MoneyRequestViewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -199,9 +203,9 @@ function MoneyRequestView({ const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => transaction?.pendingFields?.[fieldPath] ?? pendingAction; return ( - - - + + {shouldShowAnimatedBackground && } + {hasReceipt && ( ); From 7ec2e5c1100314685ed899c9f3b67c36bb2e2d4e Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 6 Feb 2024 20:43:07 -0800 Subject: [PATCH 005/173] update css style for money request combine report actions for report view --- src/libs/ReportUtils.ts | 5 +++++ src/pages/home/ReportScreen.js | 15 +++++++++++++-- src/styles/utils/index.ts | 19 ++++++++----------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9107322f0c67..b838eb5b51b6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3738,6 +3738,11 @@ function shouldReportBeInOptionList({ return false; } + // If this is a transaction thread associated with a report that only has one transaction, omit it + if (isOneTransactionThread(report)) { + return false; + } + // Include the currently viewed report. If we excluded the currently viewed report, then there // would be no way to highlight it in the options list and it would be confusing to users because they lose // a sense of context. diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index bfe27910c943..10cad0e84c0a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -68,6 +68,9 @@ const propTypes = { /** All the report actions for this report */ reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** The report actions for the first transaction thread associated with the report */ + transactionThreadReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** The report's parentReportAction */ parentReportAction: PropTypes.shape(reportActionPropTypes), @@ -103,7 +106,8 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - reportActions: {}, + reportActions: [], + transactionThreadReportActions: [], parentReportAction: {}, report: {}, reportMetadata: { @@ -143,6 +147,7 @@ function ReportScreen({ report: reportProp, reportMetadata, reportActions, + transactionThreadReportActions, parentReportAction, accountManagerReportID, markReadyForHydration, @@ -278,6 +283,7 @@ function ReportScreen({ const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); + const isOneTransactionReport = ReportUtils.isOneTransactionReport(report); useEffect(() => { if (!report || !report.reportID || shouldHideReport) { @@ -554,7 +560,7 @@ function ReportScreen({ > {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, + transactionThreadReportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ReportUtils.getOneTransactionThreadReportID(getReportID(route))}`, + canEvict: false, + selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, allowStaleData: true, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index a45b7cdbcb34..dc8fdac226f2 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -754,21 +754,18 @@ function getLineHeightStyle(lineHeight: number): TextStyle { /** * Gets the correct size for the empty state container based on screen dimensions */ -function getReportWelcomeContainerStyle(isSmallScreenWidth: boolean, isMoneyOrTaskReport = false): ViewStyle { +function getReportWelcomeContainerStyle(isSmallScreenWidth: boolean, isMoneyOrTaskReport = false, shouldShowAnimatedBackground = true): ViewStyle { const emptyStateBackground = isMoneyOrTaskReport ? CONST.EMPTY_STATE_BACKGROUND.MONEY_OR_TASK_REPORT : CONST.EMPTY_STATE_BACKGROUND; - if (isSmallScreenWidth) { - return { - minHeight: emptyStateBackground.SMALL_SCREEN.CONTAINER_MINHEIGHT, - display: 'flex', - justifyContent: 'space-between', - }; - } - - return { - minHeight: emptyStateBackground.WIDE_SCREEN.CONTAINER_MINHEIGHT, + let baseStyles: ViewStyle = { display: 'flex', justifyContent: 'space-between', }; + + if (shouldShowAnimatedBackground) { + baseStyles.minHeight = isSmallScreenWidth ? emptyStateBackground.SMALL_SCREEN.CONTAINER_MINHEIGHT : emptyStateBackground.WIDE_SCREEN.CONTAINER_MINHEIGHT; + } + + return baseStyles; } type GetBaseAutoCompleteSuggestionContainerStyleParams = { From 3c16beecf712c4300e7af4040edbee160f4dcb5e Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 7 Feb 2024 20:42:37 +0000 Subject: [PATCH 006/173] chore(typescript): migrate IOU related components --- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils.ts | 18 +- src/libs/actions/IOU.ts | 26 ++- src/pages/iou/NewDistanceRequestPage.tsx | 2 +- src/pages/iou/SplitBillDetailsPage.js | 196 ------------------- src/pages/iou/SplitBillDetailsPage.tsx | 25 ++- src/pages/iou/steps/NewRequestAmountPage.tsx | 2 +- 8 files changed, 41 insertions(+), 232 deletions(-) delete mode 100644 src/pages/iou/SplitBillDetailsPage.js diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b6518b361381..33e5bbf25cbc 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -725,7 +725,7 @@ function createOption( /** * Get the option for a policy expense report. */ -function getPolicyExpenseReportOption(report: Report): ReportUtils.OptionData { +function getPolicyExpenseReportOption(report: Partial): ReportUtils.OptionData { const expenseReport = policyExpenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; const option = createOption( diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 64d79a3cd812..c1751c7ff164 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1984,7 +1984,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< * into a flat object. Used for displaying transactions and sending them in API commands */ -function getTransactionDetails(transaction: OnyxEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING): TransactionDetails | undefined { +function getTransactionDetails(transaction: OnyxEntry | undefined, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING): TransactionDetails | undefined { if (!transaction) { return; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8a814f311481..47f52a534605 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -135,11 +135,11 @@ function hasReceipt(transaction: Transaction | undefined | null): boolean { return !!transaction?.receipt?.state || hasEReceipt(transaction); } -function isMerchantMissing(transaction: Transaction) { - if (transaction.modifiedMerchant && transaction.modifiedMerchant !== '') { - return transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; +function isMerchantMissing(transaction: Transaction | undefined) { + if (transaction?.modifiedMerchant && transaction.modifiedMerchant !== '') { + return transaction?.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; } - const isMerchantEmpty = transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.merchant === ''; + const isMerchantEmpty = transaction?.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction?.merchant === ''; return isMerchantEmpty; } @@ -151,15 +151,15 @@ function isPartialMerchant(merchant: string): boolean { return merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; } -function isAmountMissing(transaction: Transaction) { - return transaction.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0); +function isAmountMissing(transaction: Transaction | undefined) { + return transaction?.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0); } -function isCreatedMissing(transaction: Transaction) { - return transaction.created === '' && (!transaction.created || transaction.modifiedCreated === ''); +function isCreatedMissing(transaction: Transaction | undefined) { + return transaction?.created === '' && (!transaction.created || transaction.modifiedCreated === ''); } -function areRequiredFieldsEmpty(transaction: Transaction): boolean { +function areRequiredFieldsEmpty(transaction: Transaction | undefined): 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); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0ce540bea456..e9bbffb076f8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2086,9 +2086,15 @@ function startSplitBill( * @param sessionAccountID - accountID of the current user * @param sessionEmail - email of the current user */ -function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportAction, updatedTransaction: OnyxTypes.Transaction, sessionAccountID: number, sessionEmail: string) { +function completeSplitBill( + chatReportID: string, + reportAction: OnyxTypes.ReportAction, + updatedTransaction: OnyxTypes.Transaction | undefined, + sessionAccountID: number, + sessionEmail: string, +) { const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); - const {transactionID} = updatedTransaction; + const {transactionID} = updatedTransaction ?? {transactionID: ''}; const unmodifiedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; // Save optimistic updated transaction and action @@ -2149,8 +2155,8 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA }, ]; - const splitParticipants: Split[] = updatedTransaction.comment.splits ?? []; - const {modifiedAmount: amount, modifiedCurrency: currency} = updatedTransaction; + const splitParticipants: Split[] = updatedTransaction?.comment.splits ?? []; + const {modifiedAmount: amount, modifiedCurrency: currency} = updatedTransaction ?? {}; // Exclude the current user when calculating the split amount, `calculateAmount` takes it into account const splitAmount = IOUUtils.calculateAmount(splitParticipants.length - 1, amount ?? 0, currency ?? '', false); @@ -2208,13 +2214,13 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA isPolicyExpenseChat ? -splitAmount : splitAmount, currency ?? '', oneOnOneIOUReport?.reportID ?? '', - updatedTransaction.comment.comment, - updatedTransaction.modifiedCreated, + updatedTransaction?.comment.comment, + updatedTransaction?.modifiedCreated, CONST.IOU.TYPE.SPLIT, transactionID, - updatedTransaction.modifiedMerchant, - {...updatedTransaction.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, - updatedTransaction.filename, + updatedTransaction?.modifiedMerchant, + {...updatedTransaction?.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, + updatedTransaction?.filename, ); const oneOnOneCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); @@ -2223,7 +2229,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA CONST.IOU.REPORT_ACTION_TYPE.CREATE, splitAmount, currency ?? '', - updatedTransaction.comment.comment ?? '', + updatedTransaction?.comment.comment ?? '', [participant], oneOnOneTransaction.transactionID, undefined, diff --git a/src/pages/iou/NewDistanceRequestPage.tsx b/src/pages/iou/NewDistanceRequestPage.tsx index f40eb5d4fbe1..260d6d6144d2 100644 --- a/src/pages/iou/NewDistanceRequestPage.tsx +++ b/src/pages/iou/NewDistanceRequestPage.tsx @@ -36,7 +36,7 @@ function NewDistanceRequestPage({iou, report, route}: NewDistanceRequestPageProp Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report?.reportID)); return; } - IOU.navigateToNextPage(iou ?? {}, iouType, report ?? {}); + IOU.navigateToNextPage(iou, iouType, report ?? undefined); }, [iou, iouType, isEditingNewRequest, report]); return ( diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js deleted file mode 100644 index be3afb822723..000000000000 --- a/src/pages/iou/SplitBillDetailsPage.js +++ /dev/null @@ -1,196 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; -import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar'; -import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /* Onyx Props */ - - /** The personal details of the person who is logged in */ - personalDetails: personalDetailsPropType, - - /** The active report */ - report: reportPropTypes.isRequired, - - /** Array of report actions for this report */ - reportActions: PropTypes.shape(reportActionPropTypes), - - /** The current transaction */ - transaction: transactionPropTypes.isRequired, - - /** The draft transaction that holds data to be persisited on the current transaction */ - draftTransaction: transactionPropTypes, - - /** Route params */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** Report ID passed via route r/:reportID/split/details */ - reportID: PropTypes.string, - - /** ReportActionID passed via route r/split/:reportActionID */ - reportActionID: PropTypes.string, - }), - }).isRequired, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - - /** Currently logged in user email */ - email: PropTypes.string, - }).isRequired, -}; - -const defaultProps = { - personalDetails: {}, - reportActions: {}, - draftTransaction: undefined, -}; - -function SplitBillDetailsPage(props) { - const styles = useThemeStyles(); - const {reportID} = props.report; - const {translate} = useLocalize(); - const reportAction = props.reportActions[`${props.route.params.reportActionID.toString()}`]; - const participantAccountIDs = reportAction.originalMessage.participantAccountIDs; - - // In case this is workspace split bill, we manually add the workspace as the second participant of the split bill - // because we don't save any accountID in the report action's originalMessage other than the payee's accountID - let participants; - if (ReportUtils.isPolicyExpenseChat(props.report)) { - participants = [ - OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true}, props.personalDetails), - OptionsListUtils.getPolicyExpenseReportOption({...props.report, selected: true}), - ]; - } else { - participants = _.map(participantAccountIDs, (accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true}, props.personalDetails)); - } - const payeePersonalDetails = props.personalDetails[reportAction.actorAccountID]; - const participantsExcludingPayee = _.filter(participants, (participant) => participant.accountID !== reportAction.actorAccountID); - - const isScanning = TransactionUtils.hasReceipt(props.transaction) && TransactionUtils.isReceiptBeingScanned(props.transaction); - const hasSmartScanFailed = TransactionUtils.hasReceipt(props.transaction) && props.transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANFAILED; - const isEditingSplitBill = props.session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(props.transaction); - - const { - amount: splitAmount, - currency: splitCurrency, - comment: splitComment, - merchant: splitMerchant, - created: splitCreated, - category: splitCategory, - tag: splitTag, - billable: splitBillable, - } = isEditingSplitBill && props.draftTransaction ? ReportUtils.getTransactionDetails(props.draftTransaction) : ReportUtils.getTransactionDetails(props.transaction); - - const onConfirm = useCallback( - () => IOU.completeSplitBill(reportID, reportAction, props.draftTransaction, props.session.accountID, props.session.email), - [reportID, reportAction, props.draftTransaction, props.session.accountID, props.session.email], - ); - - return ( - - - - - {isScanning && ( - - )} - {Boolean(participants.length) && ( - - )} - - - - ); -} - -SplitBillDetailsPage.propTypes = propTypes; -SplitBillDetailsPage.defaultProps = defaultProps; -SplitBillDetailsPage.displayName = 'SplitBillDetailsPage'; - -export default compose( - withReportAndReportActionOrNotFound, - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - reportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, - canEvict: false, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({route, reportActions}) => { - const reportAction = reportActions[`${route.params.reportActionID.toString()}`]; - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - draftTransaction: { - key: ({route, reportActions}) => { - const reportAction = reportActions[`${route.params.reportActionID.toString()}`]; - return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), -)(SplitBillDetailsPage); diff --git a/src/pages/iou/SplitBillDetailsPage.tsx b/src/pages/iou/SplitBillDetailsPage.tsx index 742f6360e45b..ce55d4dea7e9 100644 --- a/src/pages/iou/SplitBillDetailsPage.tsx +++ b/src/pages/iou/SplitBillDetailsPage.tsx @@ -49,9 +49,9 @@ type SplitBillDetailsPageProps = WithReportAndReportActionOrNotFound & SplitBill function SplitBillDetailsPage({personalDetails, report, route, reportActions, transaction, draftTransaction, session}: SplitBillDetailsPageProps) { const styles = useThemeStyles(); - const {reportID} = report ?? {}; + const {reportID} = report ?? {reportID: ''}; const {translate} = useLocalize(); - const reportAction = reportActions?.[route.params.reportActionID] as (ReportActionBase & OriginalMessageIOU) | undefined; + const reportAction = reportActions?.[route.params.reportActionID] as ReportActionBase & OriginalMessageIOU; const participantAccountIDs = reportAction?.originalMessage.participantAccountIDs ?? []; // In case this is workspace split bill, we manually add the workspace as the second participant of the split bill @@ -59,21 +59,18 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr let participants; if (ReportUtils.isPolicyExpenseChat(report)) { participants = [ - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true}, personalDetails), + OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true, reportID: ''}, personalDetails), OptionsListUtils.getPolicyExpenseReportOption({...report, selected: true}), ]; } else { - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - participants = participantAccountIDs.map((accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true}, personalDetails)); + participants = participantAccountIDs.map((accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true, reportID: ''}, personalDetails)); } const payeePersonalDetails = personalDetails?.[reportAction?.actorAccountID ?? 0]; - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. const participantsExcludingPayee = participants.filter((participant) => participant.accountID !== reportAction?.actorAccountID); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const hasSmartScanFailed = TransactionUtils.hasReceipt(transaction) && transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCANFAILED; - const isEditingSplitBill = session?.accountID === reportAction?.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction ?? ({} as Transaction)); + const isEditingSplitBill = session?.accountID === reportAction?.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction ?? undefined); const { amount: splitAmount, @@ -83,16 +80,17 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr created: splitCreated, category: splitCategory, tag: splitTag, - } = ReportUtils.getTransactionDetails(isEditingSplitBill && draftTransaction ? draftTransaction : transaction) ?? {}; + billable: splitBillable, + } = ReportUtils.getTransactionDetails((isEditingSplitBill && draftTransaction) || transaction) ?? {}; const onConfirm = useCallback( - () => IOU.completeSplitBill(Number(reportID), reportAction ?? {}, draftTransaction ?? {}, session?.accountID ?? -1, session?.email ?? ''), + () => IOU.completeSplitBill(reportID, reportAction, draftTransaction ?? undefined, session?.accountID ?? 0, session?.email ?? ''), [reportID, reportAction, draftTransaction, session?.accountID, session?.email], ); return ( - + {isScanning && ( @@ -114,6 +112,7 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr iouMerchant={splitMerchant} iouCategory={splitCategory} iouTag={splitTag} + iouIsBillable={splitBillable} iouType={CONST.IOU.TYPE.SPLIT} isReadOnly={!isEditingSplitBill} shouldShowSmartScanFields @@ -124,10 +123,10 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr hasSmartScanFailed={hasSmartScanFailed} reportID={reportID} reportActionID={reportAction?.reportActionID} - transaction={isEditingSplitBill ? draftTransaction ?? transaction : transaction} + transaction={isEditingSplitBill ? draftTransaction || transaction : transaction} onConfirm={onConfirm} isPolicyExpenseChat={ReportUtils.isPolicyExpenseChat(report)} - policyID={ReportUtils.isPolicyExpenseChat(report) && report?.policyID} + policyID={ReportUtils.isPolicyExpenseChat(report) ? report?.policyID : null} /> )} diff --git a/src/pages/iou/steps/NewRequestAmountPage.tsx b/src/pages/iou/steps/NewRequestAmountPage.tsx index 4d530c050c22..0224c912e443 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.tsx +++ b/src/pages/iou/steps/NewRequestAmountPage.tsx @@ -118,7 +118,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}: NewRequestAmoun return; } - IOU.navigateToNextPage(iou ?? {}, iouType, report ?? {}); + IOU.navigateToNextPage(iou, iouType, report ?? undefined); }; const content = ( From b25f22f7e3c3e64299715c19b8ea43438f60bf5a Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 9 Feb 2024 17:32:22 +0000 Subject: [PATCH 007/173] chore: apply pull request suggestions --- src/pages/iou/NewDistanceRequestPage.tsx | 2 +- src/pages/iou/steps/NewRequestAmountPage.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/NewDistanceRequestPage.tsx b/src/pages/iou/NewDistanceRequestPage.tsx index 49b918dd3b30..1b35d9646daf 100644 --- a/src/pages/iou/NewDistanceRequestPage.tsx +++ b/src/pages/iou/NewDistanceRequestPage.tsx @@ -24,7 +24,7 @@ type NewDistanceRequestPageProps = NewDistanceRequestPageOnyxProps & StackScreen // This component is responsible for getting the transactionID from the IOU key, or creating the transaction if it doesn't exist yet, and then passing the transactionID. // You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that DistanceRequest can subscribe to the transaction. function NewDistanceRequestPage({iou, report, route}: NewDistanceRequestPageProps) { - const iouType = route.params.iouType ?? 'request'; + const iouType = route.params.iouType; const isEditingNewRequest = Navigation.getActiveRoute().includes('address'); useEffect(() => { diff --git a/src/pages/iou/steps/NewRequestAmountPage.tsx b/src/pages/iou/steps/NewRequestAmountPage.tsx index e1e406263825..8b63b67b0808 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.tsx +++ b/src/pages/iou/steps/NewRequestAmountPage.tsx @@ -24,6 +24,8 @@ import type SCREENS from '@src/SCREENS'; import type {IOU as IOUType, Report} from '@src/types/onyx'; import MoneyRequestAmountForm from './MoneyRequestAmountForm'; +type NavigateToNextPageOptions = {amount: string}; + type NewRequestAmountPageOnyxProps = { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: OnyxEntry; @@ -113,7 +115,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}: NewRequestAmoun Navigation.navigate(ROUTES.MONEY_REQUEST_CURRENCY.getRoute(iouType, reportID, currency, activeRoute)); }; - const navigateToNextPage = ({amount}: {amount: string}) => { + const navigateToNextPage = ({amount}: NavigateToNextPageOptions) => { const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); IOU.setMoneyRequestAmount(amountInSmallestCurrencyUnits); IOU.setMoneyRequestCurrency(currency); From 9eebd1bed31a1395be1cf6f5f6d10e1877e463db Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 9 Feb 2024 17:45:47 +0000 Subject: [PATCH 008/173] fix: typing issues --- src/libs/Navigation/OnyxTabNavigator.tsx | 10 ++++++---- src/libs/actions/Tab.ts | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index 2ae3414956a8..c0ac1b20920b 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -4,13 +4,15 @@ import type {EventMapCore, NavigationState, ScreenListeners} from '@react-naviga import React from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Tab from '@userActions/Tab'; +import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultScreenOptions} from './OnyxTabNavigatorConfig'; type OnyxTabNavigatorOnyxProps = { - selectedTab: OnyxEntry; + selectedTab: OnyxEntry>; }; type OnyxTabNavigatorProps = OnyxTabNavigatorOnyxProps & @@ -19,7 +21,7 @@ type OnyxTabNavigatorProps = OnyxTabNavigatorOnyxProps & id: string; /** Name of the selected tab */ - selectedTab?: string; + selectedTab?: ValueOf; /** A function triggered when a tab has been selected */ onTabSelected?: (newIouType: string) => void; @@ -32,7 +34,7 @@ export const TopTab = createMaterialTopTabNavigator(); // This takes all the same props as MaterialTopTabsNavigator: https://reactnavigation.org/docs/material-top-tab-navigator/#props, // except ID is now required, and it gets a `selectedTab` from Onyx -function OnyxTabNavigator({id, selectedTab = '', children, onTabSelected = () => {}, screenListeners, ...rest}: OnyxTabNavigatorProps) { +function OnyxTabNavigator({id, selectedTab, children, onTabSelected = () => {}, screenListeners, ...rest}: OnyxTabNavigatorProps) { return ( const state = event.data.state; const index = state.index; const routeNames = state.routeNames; - Tab.setSelectedTab(id, routeNames[index]); + Tab.setSelectedTab(id, routeNames[index] as ValueOf); onTabSelected(routeNames[index]); }, ...(screenListeners ?? {}), diff --git a/src/libs/actions/Tab.ts b/src/libs/actions/Tab.ts index a210cef36c73..8f1f647bd982 100644 --- a/src/libs/actions/Tab.ts +++ b/src/libs/actions/Tab.ts @@ -1,10 +1,12 @@ import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; /** * Sets the selected tab for a given tab ID */ -function setSelectedTab(id: string, index: string) { +function setSelectedTab(id: string, index: ValueOf) { Onyx.merge(`${ONYXKEYS.COLLECTION.SELECTED_TAB}${id}`, index); } From 353b74099d6162013136351775bd00a9f155c55a Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Fri, 9 Feb 2024 12:23:18 -0800 Subject: [PATCH 009/173] update report actions items to retrieve transaction thread report details --- src/pages/home/report/ReportActionItem.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 209e0c00bbbe..0f1a729794fb 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -77,6 +77,7 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; const propTypes = { ...windowDimensionsPropTypes, @@ -113,6 +114,9 @@ const propTypes = { /** IOU report for this action, if any */ iouReport: reportPropTypes, + /** Single transaction thread associated with the report, if any */ + transactionThreadReport: reportPropTypes, + /** Flag to show, hide the thread divider line */ shouldHideThreadDividerLine: PropTypes.bool, @@ -132,6 +136,7 @@ const defaultProps = { emojiReactions: {}, shouldShowSubscriptAvatar: false, iouReport: undefined, + transactionThreadReport: {}, shouldHideThreadDividerLine: false, userWallet: {}, parentReportActions: {}, @@ -664,6 +669,15 @@ function ReportActionItem(props) { policyReportFields={_.values(props.policyReportFields)} shouldShowHorizontalRule={!props.shouldHideThreadDividerLine} /> + {!isEmptyObject(props.transactionThreadReport) && ( + + + + )} ); } @@ -813,6 +827,13 @@ export default compose( }, initialValue: {}, }, + transactionThreadReport: { + key: ({report}) => { + const transactionThreadReportID = ReportUtils.isOneTransactionReport(report) ? ReportUtils.getOneTransactionThreadReportID(report) : ''; + return transactionThreadReportID ? `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}` : {}; + }, + initialValue: {}, + }, policyReportFields: { key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), initialValue: [], @@ -847,6 +868,7 @@ export default compose( _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) && + _.isEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') && lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') && lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') && From 66f498b67d2eff3ff47442ee64068518ef531c28 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Fri, 9 Feb 2024 12:40:43 -0800 Subject: [PATCH 010/173] use undefined instead of {} --- src/pages/home/report/ReportActionItem.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 0f1a729794fb..47b0650c9869 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -136,7 +136,7 @@ const defaultProps = { emojiReactions: {}, shouldShowSubscriptAvatar: false, iouReport: undefined, - transactionThreadReport: {}, + transactionThreadReport: undefined, shouldHideThreadDividerLine: false, userWallet: {}, parentReportActions: {}, @@ -669,7 +669,7 @@ function ReportActionItem(props) { policyReportFields={_.values(props.policyReportFields)} shouldShowHorizontalRule={!props.shouldHideThreadDividerLine} /> - {!isEmptyObject(props.transactionThreadReport) && ( + {props.transactionThreadReport && !isEmptyObject(props.transactionThreadReport) && ( { const transactionThreadReportID = ReportUtils.isOneTransactionReport(report) ? ReportUtils.getOneTransactionThreadReportID(report) : ''; - return transactionThreadReportID ? `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}` : {}; + return transactionThreadReportID ? `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}` : undefined; }, initialValue: {}, }, From de66987b70c4e67823775ecc39dc936ba1685320 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Fri, 9 Feb 2024 14:57:50 -0800 Subject: [PATCH 011/173] check prevProps --- src/pages/home/ReportScreen.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d37be839d8cc..b7e05af217a2 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -670,6 +670,7 @@ export default compose( (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && _.isEqual(prevProps.reportActions, nextProps.reportActions) && + _.isEqual(prevProps.transactionThreadReportActions, nextProps.transactionThreadReportActions) && _.isEqual(prevProps.reportMetadata, nextProps.reportMetadata) && prevProps.isComposerFullSize === nextProps.isComposerFullSize && _.isEqual(prevProps.betas, nextProps.betas) && From 264dbbcfb26a46f49dad8924ebe1d0d48da1dead Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 16 Feb 2024 00:18:09 +0000 Subject: [PATCH 012/173] chore: apply pull request feedback --- src/pages/iou/steps/NewRequestAmountPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/steps/NewRequestAmountPage.tsx b/src/pages/iou/steps/NewRequestAmountPage.tsx index 8b63b67b0808..8f0085230233 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.tsx +++ b/src/pages/iou/steps/NewRequestAmountPage.tsx @@ -139,7 +139,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}: NewRequestAmoun }} onCurrencyButtonPress={navigateToCurrencySelectionPage} onSubmitButtonPress={navigateToNextPage} - selectedTab={selectedTab} + selectedTab={selectedTab ?? CONST.TAB_REQUEST.MANUAL} /> ); From 1bf4c04caf038e6cbb1957bb23d0f13c3b88ca71 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Mon, 19 Feb 2024 20:08:01 -0800 Subject: [PATCH 013/173] simplify transactionThreadReportActions display --- src/libs/ReportUtils.ts | 2 +- src/pages/home/ReportScreen.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6d094087b2fa..0042f7da11d5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1218,7 +1218,7 @@ function getOneTransactionThreadReportID(reportOrID: OnyxEntry | string) // Get all IOU report actions for the report. const iouReportAction = ReportActionsUtils.getIOUReportActions(report?.reportID ?? '')?.find(reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.childReportID); - return String(iouReportAction?.childReportID) ?? '0'; + return iouReportAction ? String(iouReportAction.childReportID) : '0' } /** diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 663cd971f71e..f281843a7052 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -268,7 +268,6 @@ function ReportScreen({ const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; const isEmptyChat = useMemo(() => _.isEmpty(reportActions), [reportActions]); - // There are no reportActions at all to display and we are still in the process of loading the next set of actions. const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS_NUM.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); @@ -285,7 +284,6 @@ function ReportScreen({ const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); - const isOneTransactionReport = ReportUtils.isOneTransactionReport(report); useEffect(() => { if (!report.reportID || shouldHideReport) { @@ -565,7 +563,7 @@ function ReportScreen({ > {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, transactionThreadReportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ReportUtils.getOneTransactionThreadReportID(getReportID(route))}`, + key: ({route}) => { + const reportID = getReportID(route); + const transactionThreadReportID = reportID && ReportUtils.isOneTransactionReport(reportID) ? ReportUtils.getOneTransactionThreadReportID(reportID) : '0'; + return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}` + }, canEvict: false, selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, From 6153babfa4e8a5c5b4be981b171ba601655800cd Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 20 Feb 2024 19:36:14 -0800 Subject: [PATCH 014/173] minor style and lint updates --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/pages/home/report/ReportActionItem.js | 4 ++-- src/styles/utils/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index ae519b04ac92..89e91cd91d52 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -71,7 +71,7 @@ type MoneyRequestViewPropsWithoutTransaction = MoneyRequestViewOnyxPropsWithoutT /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; - /** Whether we should display the animated above the component */ + /** Whether we should display the animated banner above the component */ shouldShowAnimatedBackground: boolean; }; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9253457c95f1..c607a9a09a84 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -62,6 +62,7 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; @@ -77,7 +78,6 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; const propTypes = { ...windowDimensionsPropTypes, @@ -637,7 +637,7 @@ function ReportActionItem(props) { ); diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 5d92ab6ff719..c926a8e83f0e 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -757,7 +757,7 @@ function getLineHeightStyle(lineHeight: number): TextStyle { */ function getReportWelcomeContainerStyle(isSmallScreenWidth: boolean, isMoneyOrTaskReport = false, shouldShowAnimatedBackground = true): ViewStyle { const emptyStateBackground = isMoneyOrTaskReport ? CONST.EMPTY_STATE_BACKGROUND.MONEY_OR_TASK_REPORT : CONST.EMPTY_STATE_BACKGROUND; - let baseStyles: ViewStyle = { + const baseStyles: ViewStyle = { display: 'flex', justifyContent: 'space-between', }; From d436ffe6c5ba59673ced29ffc171a0241b13275b Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 20 Feb 2024 20:26:42 -0800 Subject: [PATCH 015/173] use simplified report icons when applicable --- src/libs/ReportUtils.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0042f7da11d5..e96f624ad8c7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1633,6 +1633,11 @@ function getIcons( }; const isPayer = currentUserAccountID === report?.managerID; + // For one transaction IOUs, display a simplified report icon + if (isOneTransactionReport(report)) { + return [ownerIcon]; + } + return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } @@ -4417,6 +4422,10 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean { return true; } + if (isExpenseReport(report) && isOneTransactionReport(report)) { + return true; + } + if (isWorkspaceTaskReport(report)) { return true; } From 79f02bc6e6b80c799649d3d83bb085f4c4eeb89b Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 22 Feb 2024 13:20:33 +0100 Subject: [PATCH 016/173] Fix bug with Item in report list is not highlighted and list cannot be navigated and refactore code --- .../CalendarPicker/YearPickerModal.tsx | 2 +- .../MoneyRequestConfirmationList.js | 3 -- ...oraryForRefactorRequestConfirmationList.js | 3 -- .../OptionsList/BaseOptionsList.tsx | 7 +++- src/components/OptionsList/types.ts | 3 -- .../optionsSelectorPropTypes.js | 3 -- .../SelectionList/BaseSelectionList.tsx | 7 +++- .../SelectionList/selectionListPropTypes.js | 3 -- .../StatePicker/StateSelectorModal.tsx | 2 +- src/libs/OptionsListUtils.ts | 39 ------------------- src/pages/EditReportFieldDropdownPage.tsx | 1 + src/pages/NewChatPage.tsx | 10 +---- .../BusinessTypeSelectorModal.tsx | 2 +- src/pages/ReportParticipantsPage.tsx | 1 - src/pages/RoomInvitePage.tsx | 6 --- src/pages/RoomMembersPage.tsx | 2 +- src/pages/SearchPage/index.js | 6 --- src/pages/WorkspaceSwitcherPage.tsx | 1 - src/pages/iou/IOUCurrencySelection.js | 1 - ...yForRefactorRequestParticipantsSelector.js | 8 ---- .../request/step/IOURequestStepCurrency.js | 1 - .../MoneyRequestParticipantsSelector.js | 8 ---- .../ShareLogList/BaseShareLogList.tsx | 6 --- .../PersonalDetails/CountrySelectionPage.js | 2 +- src/pages/settings/Profile/PronounsPage.js | 2 +- .../settings/Profile/TimezoneSelectPage.js | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.js | 8 ---- .../TaskShareDestinationSelectorModal.js | 3 -- src/pages/workspace/WorkspaceInvitePage.tsx | 6 --- src/pages/workspace/WorkspaceMembersPage.js | 2 +- .../workspace/WorkspaceProfileCurrencyPage.js | 2 +- 31 files changed, 23 insertions(+), 129 deletions(-) diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx index f8c4a12ec188..29beedb92714 100644 --- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx +++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx @@ -34,7 +34,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear const yearsList = searchText === '' ? years : years.filter((year) => year.text.includes(searchText)); return { headerMessage: !yearsList.length ? translate('common.noResultsFound') : '', - sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}], + sections: [{data: yearsList.sort((a, b) => b.value - a.value)}], }; }, [years, searchText, translate]); diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 0de601bc9f61..5db353a01830 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -377,14 +377,12 @@ function MoneyRequestConfirmationList(props) { title: translate('moneyRequestConfirmationList.paidBy'), data: [formattedPayeeOption], shouldShow: true, - indexOffset: 0, isDisabled: shouldDisablePaidBySection, }, { title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - indexOffset: 1, }, ); } else { @@ -396,7 +394,6 @@ function MoneyRequestConfirmationList(props) { title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, - indexOffset: 0, }); } return sections; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 3939e847707d..a2efb01a5e76 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -422,14 +422,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ title: translate('moneyRequestConfirmationList.paidBy'), data: [formattedPayeeOption], shouldShow: true, - indexOffset: 0, isDisabled: shouldDisablePaidBySection, }, { title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - indexOffset: 1, }, ); } else { @@ -441,7 +439,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, - indexOffset: 0, }); } return sections; diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 575df128894a..928308c73bfe 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -223,6 +223,11 @@ function BaseOptionsList( return ; }; + const sectionsWithIndexOffset = sections.map((section, index) => { + const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + curr.data.length, 0); + return {...section, indexOffset}; + }); + return ( {isLoading ? ( @@ -248,7 +253,7 @@ function BaseOptionsList( onScroll={onScroll} contentContainerStyle={contentContainerStyles} showsVerticalScrollIndicator={showScrollIndicator} - sections={sections} + sections={sectionsWithIndexOffset} keyExtractor={extractKey} stickySectionHeadersEnabled={false} renderItem={renderItem} diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts index fa3ef8df56f6..dc455a53690d 100644 --- a/src/components/OptionsList/types.ts +++ b/src/components/OptionsList/types.ts @@ -9,9 +9,6 @@ type Section = { /** Title of the section */ title: string; - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: number; - /** Array of options */ data: OptionData[]; diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js index 8e58a7ffdb86..b430ce8a4933 100644 --- a/src/components/OptionsSelector/optionsSelectorPropTypes.js +++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js @@ -14,9 +14,6 @@ const propTypes = { /** Title of the section */ title: PropTypes.string, - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: PropTypes.number, - /** Array of options */ data: PropTypes.arrayOf(optionPropTypes), diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index b0996a08895a..9c2998a0f868 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -375,6 +375,11 @@ function BaseSelectionList( isActive: !disableKeyboardShortcuts && isFocused, }); + const sectionsWithIndexOffset = sections.map((section, index) => { + const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + curr.data.length, 0); + return {...section, indexOffset}; + }); + return ( ( )} {}, onStat // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing textInputLabel={label || translate('common.state')} textInputValue={searchValue} - sections={[{data: searchResults, indexOffset: 0}]} + sections={[{data: searchResults}]} onSelectRow={onStateSelected} onChangeText={setSearchValue} initiallyFocusedOptionKey={currentState} diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 97b4fc0144c8..e655740eca95 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -71,7 +71,6 @@ type PayeePersonalDetails = { type CategorySection = { title: string | undefined; shouldShow: boolean; - indexOffset: number; data: Option[]; }; @@ -131,7 +130,6 @@ type MemberForList = { type SectionForSearchTerm = { section: CategorySection; - newIndexOffset: number; }; type GetOptions = { @@ -946,14 +944,11 @@ function getCategoryListSections( const categorySections: CategorySection[] = []; const numberOfCategories = enabledCategories.length; - let indexOffset = 0; - if (numberOfCategories === 0 && selectedOptions.length > 0) { categorySections.push({ // "Selected" section title: '', shouldShow: false, - indexOffset, data: getCategoryOptionTree(selectedOptions, true), }); @@ -967,7 +962,6 @@ function getCategoryListSections( // "Search" section title: '', shouldShow: true, - indexOffset, data: getCategoryOptionTree(searchCategories, true), }); @@ -983,7 +977,6 @@ function getCategoryListSections( // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getCategoryOptionTree(enabledAndSelectedCategories), }); @@ -995,11 +988,8 @@ function getCategoryListSections( // "Selected" section title: '', shouldShow: false, - indexOffset, data: getCategoryOptionTree(selectedOptions, true), }); - - indexOffset += selectedOptions.length; } const filteredRecentlyUsedCategories = recentlyUsedCategories @@ -1016,11 +1006,8 @@ function getCategoryListSections( // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - indexOffset, data: getCategoryOptionTree(cutRecentlyUsedCategories, true), }); - - indexOffset += filteredRecentlyUsedCategories.length; } const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); @@ -1029,7 +1016,6 @@ function getCategoryListSections( // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - indexOffset, data: getCategoryOptionTree(filteredCategories), }); @@ -1063,7 +1049,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt const sortedTags = sortTags(tags); const enabledTags = sortedTags.filter((tag) => tag.enabled); const numberOfTags = enabledTags.length; - let indexOffset = 0; // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { @@ -1076,7 +1061,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Selected" section title: '', shouldShow: false, - indexOffset, data: getTagsOptions(selectedTagOptions), }); @@ -1090,7 +1074,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Search" section title: '', shouldShow: true, - indexOffset, data: getTagsOptions(searchTags), }); @@ -1102,7 +1085,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getTagsOptions(enabledTags), }); @@ -1131,11 +1113,8 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Selected" section title: '', shouldShow: true, - indexOffset, data: getTagsOptions(selectedTagOptions), }); - - indexOffset += selectedOptions.length; } if (filteredRecentlyUsedTags.length > 0) { @@ -1145,18 +1124,14 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - indexOffset, data: getTagsOptions(cutRecentlyUsedTags), }); - - indexOffset += filteredRecentlyUsedTags.length; } tagSections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - indexOffset, data: getTagsOptions(filteredTags), }); @@ -1227,8 +1202,6 @@ function getTaxRatesSection(policyTaxRates: PolicyTaxRateWithDefault | undefined const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); const numberOfTaxRates = enabledTaxRates.length; - let indexOffset = 0; - // If all tax are disabled but there's a previously selected tag, show only the selected tag if (numberOfTaxRates === 0 && selectedOptions.length > 0) { const selectedTaxRateOptions = selectedOptions.map((option) => ({ @@ -1240,7 +1213,6 @@ function getTaxRatesSection(policyTaxRates: PolicyTaxRateWithDefault | undefined // "Selected" sectiong title: '', shouldShow: false, - indexOffset, data: getTaxRatesOptions(selectedTaxRateOptions), }); @@ -1254,7 +1226,6 @@ function getTaxRatesSection(policyTaxRates: PolicyTaxRateWithDefault | undefined // "Search" section title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(searchTaxRates), }); @@ -1266,7 +1237,6 @@ function getTaxRatesSection(policyTaxRates: PolicyTaxRateWithDefault | undefined // "All" section when items amount less than the threshold title: '', shouldShow: false, - indexOffset, data: getTaxRatesOptions(enabledTaxRates), }); @@ -1290,18 +1260,14 @@ function getTaxRatesSection(policyTaxRates: PolicyTaxRateWithDefault | undefined // "Selected" section title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(selectedTaxRatesOptions), }); - - indexOffset += selectedOptions.length; } policyRatesSections.push({ // "All" section when number of items are more than the threshold title: '', shouldShow: true, - indexOffset, data: getTaxRatesOptions(filteredTaxRates), }); @@ -1965,7 +1931,6 @@ function formatSectionsFromSearchTerm( filteredRecentReports: ReportUtils.OptionData[], filteredPersonalDetails: ReportUtils.OptionData[], maxOptionsSelected: boolean, - indexOffset = 0, personalDetails: OnyxEntry = {}, shouldGetOptionDetails = false, ): SectionForSearchTerm { @@ -1983,9 +1948,7 @@ function formatSectionsFromSearchTerm( }) : selectedOptions, shouldShow: selectedOptions.length > 0, - indexOffset, }, - newIndexOffset: indexOffset + selectedOptions.length, }; } @@ -2009,9 +1972,7 @@ function formatSectionsFromSearchTerm( }) : selectedParticipantsWithoutDetails, shouldShow: selectedParticipantsWithoutDetails.length > 0, - indexOffset, }, - newIndexOffset: indexOffset + selectedParticipantsWithoutDetails.length, }; } diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 1ad3c766221b..1ad63bb2bf2f 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -92,6 +92,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, textInputLabel={translate('common.search')} boldStyle sections={sections} + focusedIndex={0} value={searchValue} onSelectRow={(option: Record) => onSubmit({[fieldID]: option.text})} onChangeText={setSearchValue} diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 72393e89ae1a..b4f40eb0691b 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -73,13 +73,10 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF const sections = useMemo((): OptionsListUtils.CategorySection[] => { const sectionsList: OptionsListUtils.CategorySection[] = []; - let indexOffset = 0; - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, maxParticipantsReached, indexOffset); + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, maxParticipantsReached); sectionsList.push(formatResults.section); - indexOffset = formatResults.newIndexOffset; - if (maxParticipantsReached) { return sectionsList; } @@ -88,24 +85,19 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF title: translate('common.recents'), data: filteredRecentReports, shouldShow: filteredRecentReports.length > 0, - indexOffset, }); - indexOffset += filteredRecentReports.length; sectionsList.push({ title: translate('common.contacts'), data: filteredPersonalDetails, shouldShow: filteredPersonalDetails.length > 0, - indexOffset, }); - indexOffset += filteredPersonalDetails.length; if (filteredUserToInvite) { sectionsList.push({ title: undefined, data: [filteredUserToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx index 2db3a4fdf7ad..0f85b58bf10a 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx @@ -62,7 +62,7 @@ function BusinessTypeSelectorModal({isVisible, currentBusinessType, onBusinessTy onBackButtonPress={onClose} /> { diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 40a1b009b38d..464a63f723f9 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -97,7 +97,6 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa const sections = useMemo(() => { const sectionsArr: Sections = []; - let indexOffset = 0; if (!didScreenTransitionEnd) { return []; @@ -120,9 +119,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa sectionsArr.push({ title: undefined, data: filterSelectedOptionsFormatted, - indexOffset, }); - indexOffset += filterSelectedOptions.length; // Filtering out selected users from the search results const selectedLogins = selectedOptions.map(({login}) => login); @@ -133,15 +130,12 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, - indexOffset, }); - indexOffset += personalDetailsFormatted.length; if (hasUnselectedUserToInvite) { sectionsArr.push({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite)], - indexOffset, }); } diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index 7593857536a6..3b33ced25d5f 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -265,7 +265,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { { const newSections = []; - let indexOffset = 0; if (recentReports.length > 0) { newSections.push({ data: recentReports, shouldShow: true, - indexOffset, }); - indexOffset += recentReports.length; } if (localPersonalDetails.length > 0) { newSections.push({ data: localPersonalDetails, shouldShow: true, - indexOffset, }); - indexOffset += recentReports.length; } if (userToInvite) { newSections.push({ data: [userToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index d361ba5137b6..7ef1c8d1fb53 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -155,7 +155,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { () => ({ data: filteredAndSortedUserWorkspaces, shouldShow: true, - indexOffset: 0, }), [filteredAndSortedUserWorkspaces], ); diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 7495efb43171..da662b066176 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -145,7 +145,6 @@ function IOUCurrencySelection(props) { : [ { data: filteredCurrencies, - indexOffset: 0, }, ], headerMessage: isEmpty ? translate('common.noResultsFound') : '', diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 238b66c0e727..c514dd9663df 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -106,7 +106,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ if (!didScreenTransitionEnd) { return [newSections, {}]; } - let indexOffset = 0; const chatOptions = OptionsListUtils.getFilteredOptions( reports, @@ -141,12 +140,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ chatOptions.recentReports, chatOptions.personalDetails, maxParticipantsReached, - indexOffset, personalDetails, true, ); newSections.push(formatResults.section); - indexOffset = formatResults.newIndexOffset; if (maxParticipantsReached) { return [newSections, {}]; @@ -156,17 +153,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ title: translate('common.recents'), data: chatOptions.recentReports, shouldShow: !_.isEmpty(chatOptions.recentReports), - indexOffset, }); - indexOffset += chatOptions.recentReports.length; newSections.push({ title: translate('common.contacts'), data: chatOptions.personalDetails, shouldShow: !_.isEmpty(chatOptions.personalDetails), - indexOffset, }); - indexOffset += chatOptions.personalDetails.length; if (chatOptions.userToInvite && !OptionsListUtils.isCurrentUser(chatOptions.userToInvite)) { newSections.push({ @@ -176,7 +169,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), shouldShow: true, - indexOffset, }); } diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.js b/src/pages/iou/request/step/IOURequestStepCurrency.js index 43e4e9bf0eaa..ba1354b4a2e6 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.js +++ b/src/pages/iou/request/step/IOURequestStepCurrency.js @@ -109,7 +109,6 @@ function IOURequestStepCurrency({ : [ { data: filteredCurrencies, - indexOffset: 0, }, ], headerMessage: isEmpty ? translate('common.noResultsFound') : '', diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..f75bf3f7ddd2 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -140,7 +140,6 @@ function MoneyRequestParticipantsSelector({ */ const sections = useMemo(() => { const newSections = []; - let indexOffset = 0; const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( searchTerm, @@ -148,12 +147,10 @@ function MoneyRequestParticipantsSelector({ newChatOptions.recentReports, newChatOptions.personalDetails, maxParticipantsReached, - indexOffset, personalDetails, true, ); newSections.push(formatResults.section); - indexOffset = formatResults.newIndexOffset; if (maxParticipantsReached) { return newSections; @@ -163,17 +160,13 @@ function MoneyRequestParticipantsSelector({ title: translate('common.recents'), data: newChatOptions.recentReports, shouldShow: !_.isEmpty(newChatOptions.recentReports), - indexOffset, }); - indexOffset += newChatOptions.recentReports.length; newSections.push({ title: translate('common.contacts'), data: newChatOptions.personalDetails, shouldShow: !_.isEmpty(newChatOptions.personalDetails), - indexOffset, }); - indexOffset += newChatOptions.personalDetails.length; if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { newSections.push({ @@ -183,7 +176,6 @@ function MoneyRequestParticipantsSelector({ return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), shouldShow: true, - indexOffset, }); } diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 18e936f3045e..70c2d301b9ac 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -64,29 +64,23 @@ function BaseShareLogList({betas, reports, onAttachLogToReport}: BaseShareLogLis const sections = useMemo(() => { const sectionsList = []; - let indexOffset = 0; sectionsList.push({ title: translate('common.recents'), data: searchOptions.recentReports, shouldShow: searchOptions.recentReports?.length > 0, - indexOffset, }); - indexOffset += searchOptions.recentReports?.length; sectionsList.push({ title: translate('common.contacts'), data: searchOptions.personalDetails, shouldShow: searchOptions.personalDetails?.length > 0, - indexOffset, }); - indexOffset += searchOptions.personalDetails?.length; if (searchOptions.userToInvite) { sectionsList.push({ data: [searchOptions.userToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js index d8327041538d..4adee2cc0dd4 100644 --- a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js +++ b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js @@ -93,7 +93,7 @@ function CountrySelectionPage({route, navigation}) { headerMessage={headerMessage} textInputLabel={translate('common.country')} textInputValue={searchValue} - sections={[{data: searchResults, indexOffset: 0}]} + sections={[{data: searchResults}]} ListItem={RadioListItem} onSelectRow={selectCountry} onChangeText={setSearchValue} diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.js index 1d4675a42b8a..b8c0a4ebffde 100644 --- a/src/pages/settings/Profile/PronounsPage.js +++ b/src/pages/settings/Profile/PronounsPage.js @@ -100,7 +100,7 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { textInputLabel={translate('pronounsPage.pronouns')} textInputPlaceholder={translate('pronounsPage.placeholderText')} textInputValue={searchValue} - sections={[{data: filteredPronounsList, indexOffset: 0}]} + sections={[{data: filteredPronounsList}]} ListItem={RadioListItem} onSelectRow={updatePronouns} onChangeText={setSearchValue} diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js index b6c8a5967abc..9e8dafcc5205 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.js +++ b/src/pages/settings/Profile/TimezoneSelectPage.js @@ -94,7 +94,7 @@ function TimezoneSelectPage(props) { textInputValue={timezoneInputText} onChangeText={filterShownTimezones} onSelectRow={saveSelectedTimezone} - sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]} + sections={[{data: timezoneOptions, isDisabled: timezone.automatic}]} initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')} showScrollIndicator shouldShowTooltips={false} diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js index 0e1e64dfa415..67a6e5f57d01 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.js @@ -121,39 +121,31 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { const sections = useMemo(() => { const sectionsList = []; - let indexOffset = 0; if (currentUserOption) { sectionsList.push({ title: translate('newTaskPage.assignMe'), data: [currentUserOption], shouldShow: true, - indexOffset, }); - indexOffset += 1; } sectionsList.push({ title: translate('common.recents'), data: recentReports, shouldShow: recentReports?.length > 0, - indexOffset, }); - indexOffset += recentReports?.length; sectionsList.push({ title: translate('common.contacts'), data: personalDetails, shouldShow: personalDetails?.length > 0, - indexOffset, }); - indexOffset += personalDetails?.length; if (userToInvite) { sectionsList.push({ data: [userToInvite], shouldShow: true, - indexOffset, }); } diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index b8d9229e6158..e4144f8b348e 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -90,15 +90,12 @@ function TaskShareDestinationSelectorModal(props) { const getSections = () => { const sections = []; - let indexOffset = 0; if (filteredRecentReports?.length > 0) { sections.push({ data: filteredRecentReports, shouldShow: true, - indexOffset, }); - indexOffset += filteredRecentReports?.length; } return sections; diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 8efc7d7c6a1e..b65f168fd7b2 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -164,7 +164,6 @@ function WorkspaceInvitePage({ const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; - let indexOffset = 0; if (!didScreenTransitionEnd) { return []; @@ -188,9 +187,7 @@ function WorkspaceInvitePage({ title: undefined, data: filterSelectedOptions, shouldShow: true, - indexOffset, }); - indexOffset += filterSelectedOptions.length; // Filtering out selected users from the search results const selectedLogins = selectedOptions.map(({login}) => login); @@ -201,9 +198,7 @@ function WorkspaceInvitePage({ title: translate('common.contacts'), data: personalDetailsFormatted, shouldShow: !isEmptyObject(personalDetailsFormatted), - indexOffset, }); - indexOffset += personalDetailsFormatted.length; Object.values(usersToInvite).forEach((userToInvite) => { const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); @@ -213,7 +208,6 @@ function WorkspaceInvitePage({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite)], shouldShow: true, - indexOffset: indexOffset++, }); } }); diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 62b96943453c..311be0481f84 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -488,7 +488,7 @@ function WorkspaceMembersPage(props) { Date: Thu, 22 Feb 2024 12:32:52 +0000 Subject: [PATCH 017/173] chore: remove unused typescript expect error --- src/pages/iou/steps/NewRequestAmountPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/steps/NewRequestAmountPage.tsx b/src/pages/iou/steps/NewRequestAmountPage.tsx index 8f0085230233..e3c6640c11e7 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.tsx +++ b/src/pages/iou/steps/NewRequestAmountPage.tsx @@ -2,13 +2,13 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import type {TextInput as RNTextInput} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -44,7 +44,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}: NewRequestAmoun const {translate} = useLocalize(); const prevMoneyRequestID = useRef(iou?.id); - const textInput = useRef(null); + const textInput = useRef(null); const iouType = route.params.iouType ?? ''; const reportID = route.params.reportID ?? ''; @@ -130,7 +130,6 @@ function NewRequestAmountPage({route, iou, report, selectedTab}: NewRequestAmoun const content = ( Date: Thu, 22 Feb 2024 13:38:57 +0100 Subject: [PATCH 018/173] Fix TS issue and update tests --- .../OptionsList/BaseOptionsList.tsx | 2 +- src/components/OptionsList/types.ts | 3 +++ tests/unit/OptionsListUtilsTest.js | 20 ------------------- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 928308c73bfe..6e3effc24e90 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -182,7 +182,7 @@ function BaseOptionsList( option={item} showTitleTooltip={showTitleTooltip} hoverStyle={optionHoveredStyle} - optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset} + optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + (section.indexOffset ?? 0)} onSelectRow={onSelectRow} isSelected={isSelected} showSelectedState={canSelectMultipleOptions} diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts index dc455a53690d..c8c117d800e4 100644 --- a/src/components/OptionsList/types.ts +++ b/src/components/OptionsList/types.ts @@ -9,6 +9,9 @@ type Section = { /** Title of the section */ title: string; + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset?: number; + /** Array of options */ data: OptionData[]; diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 00f1307ab59f..cd17b5157059 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -713,7 +713,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Food', @@ -746,7 +745,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Food', @@ -771,7 +769,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -837,7 +834,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Medical', @@ -852,7 +848,6 @@ describe('OptionsListUtils', () => { { title: 'Recent', shouldShow: true, - indexOffset: 1, data: [ { text: 'Restaurant', @@ -867,7 +862,6 @@ describe('OptionsListUtils', () => { { title: 'All', shouldShow: true, - indexOffset: 2, data: [ { text: 'Cars', @@ -964,7 +958,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Food', @@ -997,7 +990,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -1006,7 +998,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, data: [ { text: 'Medical', @@ -1111,7 +1102,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -1142,7 +1132,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Accounting', @@ -1158,7 +1147,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -1212,7 +1200,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Medical', @@ -1226,7 +1213,6 @@ describe('OptionsListUtils', () => { { title: 'Recent', shouldShow: true, - indexOffset: 1, data: [ { text: 'HR', @@ -1240,7 +1226,6 @@ describe('OptionsListUtils', () => { { title: 'All', shouldShow: true, - indexOffset: 2, // data sorted alphabetically by name data: [ { @@ -1299,7 +1284,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [ { text: 'Accounting', @@ -1322,7 +1306,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; @@ -2088,7 +2071,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: false, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -2141,7 +2123,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, // data sorted alphabetically by name data: [ { @@ -2165,7 +2146,6 @@ describe('OptionsListUtils', () => { { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; From 049756b984e4918117b5bfa2ffbbd6b8ee04879f Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 22 Feb 2024 15:32:24 +0100 Subject: [PATCH 019/173] Update tests --- tests/perf-test/OptionsSelector.perf-test.js | 14 +++++--------- tests/perf-test/SelectionList.perf-test.js | 1 - 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/perf-test/OptionsSelector.perf-test.js b/tests/perf-test/OptionsSelector.perf-test.js index 6104ded05c6a..969015ae0fbc 100644 --- a/tests/perf-test/OptionsSelector.perf-test.js +++ b/tests/perf-test/OptionsSelector.perf-test.js @@ -36,21 +36,17 @@ jest.mock('../../src/components/withNavigationFocus', () => (Component) => { }); const generateSections = (sectionConfigs) => - _.map(sectionConfigs, ({numItems, indexOffset, shouldShow = true}) => ({ + _.map(sectionConfigs, ({numItems, shouldShow = true}, index) => ({ data: Array.from({length: numItems}, (_v, i) => ({ - text: `Item ${i + indexOffset}`, - keyForList: `item-${i + indexOffset}`, + text: `Item ${i + index}`, + keyForList: `item-${i + index}`, })), - indexOffset, shouldShow, })); -const singleSectionSConfig = [{numItems: 1000, indexOffset: 0}]; +const singleSectionSConfig = [{numItems: 1000}]; -const mutlipleSectionsConfig = [ - {numItems: 1000, indexOffset: 0}, - {numItems: 100, indexOffset: 70}, -]; +const mutlipleSectionsConfig = [{numItems: 1000}, {numItems: 100}]; function OptionsSelectorWrapper(args) { const sections = generateSections(singleSectionSConfig); diff --git a/tests/perf-test/SelectionList.perf-test.js b/tests/perf-test/SelectionList.perf-test.js index a109f92a1501..4afdca30313f 100644 --- a/tests/perf-test/SelectionList.perf-test.js +++ b/tests/perf-test/SelectionList.perf-test.js @@ -64,7 +64,6 @@ function SelectionListWrapper(args) { keyForList: `item-${i}`, isSelected: _.contains(selectedIds, `item-${i}`), })), - indexOffset: 0, isDisabled: false, }, ]; From ffbe73a35218775b153099c0544751217fdb2414 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 22 Feb 2024 14:39:27 +0000 Subject: [PATCH 020/173] [TS migration] Migrate awaitStagingDeploysTest to Typescript --- src/types/utils/AsMutable.ts | 5 +++ ...loysTest.js => awaitStagingDeploysTest.ts} | 39 ++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 src/types/utils/AsMutable.ts rename tests/unit/{awaitStagingDeploysTest.js => awaitStagingDeploysTest.ts} (83%) diff --git a/src/types/utils/AsMutable.ts b/src/types/utils/AsMutable.ts new file mode 100644 index 000000000000..57c49058cd14 --- /dev/null +++ b/src/types/utils/AsMutable.ts @@ -0,0 +1,5 @@ +import type {Writable} from 'type-fest'; + +const asMutable = (value: T): Writable => value as Writable; + +export default asMutable; diff --git a/tests/unit/awaitStagingDeploysTest.js b/tests/unit/awaitStagingDeploysTest.ts similarity index 83% rename from tests/unit/awaitStagingDeploysTest.js rename to tests/unit/awaitStagingDeploysTest.ts index 8b8327e99047..7f1d6bac1eaa 100644 --- a/tests/unit/awaitStagingDeploysTest.js +++ b/tests/unit/awaitStagingDeploysTest.ts @@ -1,29 +1,46 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + /** * @jest-environment node */ import * as core from '@actions/core'; -import _ from 'underscore'; +import asMutable from '@src/types/utils/AsMutable'; import run from '../../.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys'; import GithubUtils from '../../.github/libs/GithubUtils'; +type Workflow = { + workflow_id: string; + branch: string; +}; + +type WorkflowStatus = {status: string}; + // Lower poll rate to speed up tests const TEST_POLL_RATE = 1; -const COMPLETED_WORKFLOW = {status: 'completed'}; -const INCOMPLETE_WORKFLOW = {status: 'in_progress'}; +const COMPLETED_WORKFLOW: WorkflowStatus = {status: 'completed'}; +const INCOMPLETE_WORKFLOW: WorkflowStatus = {status: 'in_progress'}; + +type MockListResponse = { + data: { + workflow_runs: WorkflowStatus[]; + }; +}; + +type MockedFunctionListResponse = jest.MockedFunction<() => Promise>; const consoleSpy = jest.spyOn(console, 'log'); const mockGetInput = jest.fn(); -const mockListPlatformDeploysForTag = jest.fn(); -const mockListPlatformDeploys = jest.fn(); -const mockListPreDeploys = jest.fn(); -const mockListWorkflowRuns = jest.fn().mockImplementation((args) => { +const mockListPlatformDeploysForTag: MockedFunctionListResponse = jest.fn(); +const mockListPlatformDeploys: MockedFunctionListResponse = jest.fn(); +const mockListPreDeploys: MockedFunctionListResponse = jest.fn(); +const mockListWorkflowRuns = jest.fn().mockImplementation((args: Workflow) => { const defaultReturn = Promise.resolve({data: {workflow_runs: []}}); - if (!_.has(args, 'workflow_id')) { + if (!args.workflow_id) { return defaultReturn; } - if (!_.isUndefined(args.branch)) { + if (args.branch !== undefined) { return mockListPlatformDeploysForTag(); } @@ -40,7 +57,7 @@ const mockListWorkflowRuns = jest.fn().mockImplementation((args) => { beforeAll(() => { // Mock core module - core.getInput = mockGetInput; + asMutable(core).getInput = mockGetInput; // Mock octokit module const moctokit = { @@ -50,6 +67,8 @@ beforeAll(() => { }, }, }; + + // @ts-expect-error TODO: Remove this once GithubUtils (https://github.com/Expensify/App/issues/25382) is migrated to TypeScript. GithubUtils.internalOctokit = moctokit; GithubUtils.POLL_RATE = TEST_POLL_RATE; }); From 452d012b89174e0e03ede3d6abd5448217c99d09 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Mon, 26 Feb 2024 22:47:47 -0800 Subject: [PATCH 021/173] add transactionThreadReportID to report structure for onyx and props --- src/pages/reportPropTypes.js | 3 +++ src/types/onyx/Report.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index 3a056ee7c0a3..7422bad8061f 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -73,4 +73,7 @@ export default PropTypes.shape({ /** Custom fields attached to the report */ reportFields: PropTypes.objectOf(PropTypes.string), + + /** ID of the transaction thread associated with the report, if any */ + transactionThreadReportID: PropTypes.string, }); diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index bb86d2cf4ae4..31ee82d6d795 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -170,6 +170,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** If the report contains reportFields, save the field id and its value */ reportFields?: Record; + + /** The ID of the single transaction thread report associated with this report, if one exists */ + transactionThreadReportID?: string }, PolicyReportField['fieldID'] >; From cac6d81a379c9e5be9b55224e4279b2e959fde1c Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Mon, 26 Feb 2024 22:50:31 -0800 Subject: [PATCH 022/173] simplify transactionThreadReportID logic to use value returned in the report from auth --- src/libs/ReportUtils.ts | 35 +++++++---------------- src/pages/home/ReportScreen.js | 7 ++--- src/pages/home/report/ReportActionItem.js | 5 +--- 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e622ebcbbc53..ccc8d4b5d5ec 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1201,33 +1201,20 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { /** * Checks if a report has only one transaction associated with it */ -function isOneTransactionReport(reportOrID: OnyxEntry | string): boolean { - const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; - - // Check the parent report (which would be the IOU or expense report if the passed report is an IOU or expense request) - // to see how many IOU report actions it contains - const iouReportActions = ReportActionsUtils.getIOUReportActions(report?.reportID ?? ''); - return (iouReportActions?.length ?? 0) === 1; -} - -/** - * Returns the reportID of the first transaction thread associated with a report - */ -function getOneTransactionThreadReportID(reportOrID: OnyxEntry | string): string | undefined { - const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; - - // Get all IOU report actions for the report. - const iouReportAction = ReportActionsUtils.getIOUReportActions(report?.reportID ?? '')?.find(reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.childReportID); - return iouReportAction ? String(iouReportAction.childReportID) : '0' +function isOneTransactionReport(report: OnyxEntry): boolean { + return report?.transactionThreadReportID !== undefined; } /** * Checks if a report is a transaction thread associated with a report that has only one transaction */ -function isOneTransactionThread(reportOrID: OnyxEntry | string): boolean { - const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; - const parentReport = getParentReport(report); - return isOneTransactionReport(parentReport?.reportID ?? ''); +function isOneTransactionThread(reportID: string, parentReport: OnyxEntry | EmptyObject): boolean { + if (isEmptyObject(parentReport)) { + return false; + } + + const transactionThreadReportID = parentReport?.transactionThreadReportID ?? undefined; + return reportID === transactionThreadReportID; } /** @@ -3967,7 +3954,8 @@ function shouldReportBeInOptionList({ } // If this is a transaction thread associated with a report that only has one transaction, omit it - if (isOneTransactionThread(report)) { + const parentReport = getParentReport(report); + if (isOneTransactionThread(report.reportID, parentReport)) { return false; } @@ -5206,7 +5194,6 @@ export { getReportRecipientAccountIDs, isOneOnOneChat, isOneTransactionReport, - getOneTransactionThreadReportID, isOneTransactionThread, goBackToDetailsPage, getTransactionReportName, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index f281843a7052..f3b12cd5c898 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -212,6 +212,7 @@ function ReportScreen({ policyName: reportProp.policyName, isOptimisticReport: reportProp.isOptimisticReport, lastMentionedTime: reportProp.lastMentionedTime, + transactionThreadReportID: reportProp.transactionThreadReportID }), [ reportProp.lastReadTime, @@ -617,11 +618,7 @@ export default compose( selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, transactionThreadReportActions: { - key: ({route}) => { - const reportID = getReportID(route); - const transactionThreadReportID = reportID && ReportUtils.isOneTransactionReport(reportID) ? ReportUtils.getOneTransactionThreadReportID(reportID) : '0'; - return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}` - }, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.transactionThreadReportID : 0}`, canEvict: false, selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 7c0b5ec57266..444badac0600 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -878,10 +878,7 @@ export default compose( initialValue: {}, }, transactionThreadReport: { - key: ({report}) => { - const transactionThreadReportID = ReportUtils.isOneTransactionReport(report) ? ReportUtils.getOneTransactionThreadReportID(report) : ''; - return transactionThreadReportID ? `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}` : undefined; - }, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.transactionThreadReportID || 0}`, initialValue: {}, }, policyReportFields: { From 6a08ba55ac4fa485b9fc605f7ae1c2e4781abcc6 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Mon, 26 Feb 2024 22:57:06 -0800 Subject: [PATCH 023/173] minor style --- src/libs/ReportActionsUtils.ts | 5 ++--- src/pages/home/ReportScreen.js | 8 ++++++-- src/types/onyx/Report.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f909823767cd..47323ad8cd56 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -533,15 +533,14 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | ReportA * are ready for display in the ReportActionView. */ function getCombinedReportActionsForDisplay(reportActions: ReportAction[], transactionThreadReportActions: ReportAction[]): ReportAction[] { - // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action - const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter(action => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); + const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); // Sort the combined list of parent report actions and transaction thread report actions const sortedReportActions = getSortedReportActions([...reportActions, ...filteredTransactionThreadReportActions], true); // Filter out IOU report actions because we don't want to show any preview actions for one transaction reports - return sortedReportActions.filter(action => action.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU); + return sortedReportActions.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU); } /** diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index f3b12cd5c898..fe622c5ccccb 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -212,7 +212,7 @@ function ReportScreen({ policyName: reportProp.policyName, isOptimisticReport: reportProp.isOptimisticReport, lastMentionedTime: reportProp.lastMentionedTime, - transactionThreadReportID: reportProp.transactionThreadReportID + transactionThreadReportID: reportProp.transactionThreadReportID, }), [ reportProp.lastReadTime, @@ -564,7 +564,11 @@ function ReportScreen({ > {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( ; /** The ID of the single transaction thread report associated with this report, if one exists */ - transactionThreadReportID?: string + transactionThreadReportID?: string; }, PolicyReportField['fieldID'] >; From 2317ae5055e79e2f1800c1e39fe196ffc6e5418d Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 27 Feb 2024 19:48:44 +0100 Subject: [PATCH 024/173] Refactore code and add new utilit for sections --- src/components/OptionsList/BaseOptionsList.tsx | 7 ++----- src/components/SelectionList/BaseSelectionList.tsx | 8 +++----- src/libs/getSectionsWithIndexOffset.ts | 9 +++++++++ 3 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 src/libs/getSectionsWithIndexOffset.ts diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 6e3effc24e90..50e0ce31fcc2 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -9,6 +9,7 @@ import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset'; import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; @@ -67,6 +68,7 @@ function BaseOptionsList( const listContainerStyles = useMemo(() => listContainerStylesProp ?? [styles.flex1], [listContainerStylesProp, styles.flex1]); const contentContainerStyles = useMemo(() => [safeAreaPaddingBottomStyle, contentContainerStylesProp], [contentContainerStylesProp, safeAreaPaddingBottomStyle]); + const sectionsWithIndexOffset = getSectionsWithIndexOffset(sections); /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. @@ -223,11 +225,6 @@ function BaseOptionsList( return ; }; - const sectionsWithIndexOffset = sections.map((section, index) => { - const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + curr.data.length, 0); - return {...section, indexOffset}; - }); - return ( {isLoading ? ( diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index fbaa5f77e9d0..1cb86e58123c 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -18,6 +18,7 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset'; import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -78,6 +79,8 @@ function BaseSelectionList( const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true); + const sectionsWithIndexOffset = getSectionsWithIndexOffset(sections); + /** * Iterates through the sections and items inside each section, and builds 3 arrays along the way: * - `allOptions`: Contains all the items in the list, flattened, regardless of section @@ -377,11 +380,6 @@ function BaseSelectionList( isActive: !disableKeyboardShortcuts && isFocused, }); - const sectionsWithIndexOffset = sections.map((section, index) => { - const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + curr.data.length, 0); - return {...section, indexOffset}; - }); - return ( (sections: Array>) { + return sections.map((section, index) => { + const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + curr.data.length, 0); + return {...section, indexOffset}; + }); +} From 279cf4a5db74ecd7abea1787fbcfd8cc231e0af6 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 27 Feb 2024 21:35:01 +0100 Subject: [PATCH 025/173] Update getSectionsWithIndexOffset --- src/libs/getSectionsWithIndexOffset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/getSectionsWithIndexOffset.ts b/src/libs/getSectionsWithIndexOffset.ts index 522412ab0949..e6c0820374e8 100644 --- a/src/libs/getSectionsWithIndexOffset.ts +++ b/src/libs/getSectionsWithIndexOffset.ts @@ -3,7 +3,7 @@ import type {SectionListData} from 'react-native'; /** Returns a list of sections with IndexOffset */ export default function getSectionsWithIndexOffset(sections: Array>) { return sections.map((section, index) => { - const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + curr.data.length, 0); + const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + (curr.data?.length ?? 0), 0); return {...section, indexOffset}; }); } From 7eb76d666f4e0fbd2c6932cacb36931a4273020c Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 27 Feb 2024 21:39:23 +0100 Subject: [PATCH 026/173] Remove unnecessary code --- src/components/SelectionList/BaseSelectionList.tsx | 1 - src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 1cb86e58123c..f3d7bc92df13 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -78,7 +78,6 @@ function BaseSelectionList( const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true); - const sectionsWithIndexOffset = getSectionsWithIndexOffset(sections); /** diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 7cd9972a6f57..a41d6158303f 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -128,7 +128,7 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP {categoryList.length ? ( Date: Tue, 27 Feb 2024 14:40:53 -0800 Subject: [PATCH 027/173] don't show report if transaction thread and expense report have the same currency minor style --- src/pages/home/ReportScreen.js | 1 + src/pages/home/report/ReportActionItem.js | 39 +++++++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index fe622c5ccccb..620ad4e23586 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -250,6 +250,7 @@ function ReportScreen({ reportProp.policyName, reportProp.isOptimisticReport, reportProp.lastMentionedTime, + reportProp.transactionThreadReportID, ], ); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 444badac0600..3baba4e2ea22 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -701,20 +701,31 @@ function ReportActionItem(props) { if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { return ( - - {props.transactionThreadReport && !isEmptyObject(props.transactionThreadReport) && ( - - - + {props.transactionThreadReport && !isEmptyObject(props.transactionThreadReport) ? ( + <> + {props.transactionThreadReport.currency !== props.report.currency && ( + + )} + + + + + ) : ( + )} ); From 90bc36177c0e4a5405e9620f1ae7cadafd2c1bcb Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 27 Feb 2024 16:37:32 -0800 Subject: [PATCH 028/173] update default value for transactionThreadReportActions key --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 620ad4e23586..d0d74274a14e 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -623,7 +623,7 @@ export default compose( selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, transactionThreadReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.transactionThreadReportID : 0}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? (report.transactionThreadReportID || '0') : '0'}`, canEvict: false, selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, From fc784e69167325e0724fb7733560ec56a0c266fe Mon Sep 17 00:00:00 2001 From: Yauheni Date: Wed, 28 Feb 2024 21:19:07 +0100 Subject: [PATCH 029/173] Remove unnecessary code --- src/components/MoneyRequestConfirmationList.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 773e98b6462e..c9d4929e1b48 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -56,7 +56,6 @@ type Option = Partial; type CategorySection = { title: string | undefined; shouldShow: boolean; - indexOffset: number; data: Option[]; }; @@ -386,14 +385,12 @@ function MoneyRequestConfirmationList({ title: translate('moneyRequestConfirmationList.paidBy'), data: [formattedPayeeOption], shouldShow: true, - indexOffset: 0, isDisabled: canModifyParticipantsValue, }, { title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - indexOffset: 1, }, ); } else { @@ -405,7 +402,6 @@ function MoneyRequestConfirmationList({ title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, - indexOffset: 0, }); } return sections; From 7b98ae0c1abad9144d84c5469a2749ed8e3a0c61 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 08:57:14 +0100 Subject: [PATCH 030/173] update libraries to newer version with types, install types --- package-lock.json | 187 ++++++++++++++++++++++------------------------ package.json | 3 +- 2 files changed, 90 insertions(+), 100 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f55ddd82868..eb1f2ca1ed91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -184,6 +184,7 @@ "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", "@types/underscore": "^1.11.5", + "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "@vercel/ncc": "0.38.1", @@ -198,7 +199,7 @@ "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^3.0.0", "concurrently": "^5.3.0", - "copy-webpack-plugin": "^6.4.1", + "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", @@ -21189,6 +21190,26 @@ "source-map": "^0.6.0" } }, + "node_modules/@types/webpack-bundle-analyzer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", + "integrity": "sha512-c5i2ThslSNSG8W891BRvOd/RoCjI2zwph8maD22b1adtSns20j+0azDDMCK06DiVrzTgnwiDl5Ntmu1YRJw8Sg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@types/webpack-bundle-analyzer/node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@types/webpack-env": { "version": "1.18.0", "dev": true, @@ -26724,155 +26745,123 @@ } }, "node_modules/copy-webpack-plugin": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz", - "integrity": "sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", + "integrity": "sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==", "dev": true, - "license": "MIT", "dependencies": { - "cacache": "^15.0.5", - "fast-glob": "^3.2.4", - "find-cache-dir": "^3.3.1", - "glob-parent": "^5.1.1", - "globby": "^11.0.1", - "loader-utils": "^2.0.0", + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", "normalize-path": "^3.0.0", - "p-limit": "^3.0.2", - "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", - "webpack-sources": "^1.4.3" + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.20.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "license": "MIT", "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" + "fast-deep-equal": "^3.1.3" }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/copy-webpack-plugin/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/copy-webpack-plugin/node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, - "node_modules/copy-webpack-plugin/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", + "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", "dev": true, - "license": "MIT", "dependencies": { - "semver": "^6.0.0" + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">=8" + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/copy-webpack-plugin/node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, - "license": "MIT", "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "randombytes": "^2.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/path-exists": { + "node_modules/copy-webpack-plugin/node_modules/slash": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/copy-webpack-plugin/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" + "node": ">=12" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/copy-webpack-plugin/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" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/core-js": { diff --git a/package.json b/package.json index e3c23d4538d3..b15951d49242 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,7 @@ "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", "@types/underscore": "^1.11.5", + "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "@vercel/ncc": "0.38.1", @@ -246,7 +247,7 @@ "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^3.0.0", "concurrently": "^5.3.0", - "copy-webpack-plugin": "^6.4.1", + "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", From 630b8aea826dec37f812633766d44b46e9f119c6 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 08:58:54 +0100 Subject: [PATCH 031/173] migrate electronBuilder.config.js to TypeScript --- ...Builder.config.js => electronBuilder.config.ts} | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename config/{electronBuilder.config.js => electronBuilder.config.ts} (74%) diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.ts similarity index 74% rename from config/electronBuilder.config.js rename to config/electronBuilder.config.ts index e4ed685f65fe..219da2703c62 100644 --- a/config/electronBuilder.config.js +++ b/config/electronBuilder.config.ts @@ -2,25 +2,25 @@ const {version} = require('../package.json'); const pullRequestNumber = process.env.PULL_REQUEST_NUMBER; -const s3Bucket = { +const s3Bucket: Record = { production: 'expensify-cash', staging: 'staging-expensify-cash', adhoc: 'ad-hoc-expensify-cash', }; -const s3Path = { +const s3Path: Record = { production: '/', staging: '/', adhoc: process.env.PULL_REQUEST_NUMBER ? `/desktop/${pullRequestNumber}/` : '/', }; -const macIcon = { +const macIcon: Record = { production: './desktop/icon.png', staging: './desktop/icon-stg.png', adhoc: './desktop/icon-adhoc.png', }; -const isCorrectElectronEnv = ['production', 'staging', 'adhoc'].includes(process.env.ELECTRON_ENV); +const isCorrectElectronEnv: boolean = ['production', 'staging', 'adhoc'].includes(process.env.ELECTRON_ENV ?? ''); if (!isCorrectElectronEnv) { throw new Error('Invalid ELECTRON_ENV!'); @@ -37,7 +37,7 @@ module.exports = { }, mac: { category: 'public.app-category.finance', - icon: macIcon[process.env.ELECTRON_ENV], + icon: process.env.ELECTRON_ENV ? macIcon[process.env.ELECTRON_ENV] : undefined, hardenedRuntime: true, entitlements: 'desktop/entitlements.mac.plist', entitlementsInherit: 'desktop/entitlements.mac.plist', @@ -54,9 +54,9 @@ module.exports = { publish: [ { provider: 's3', - bucket: s3Bucket[process.env.ELECTRON_ENV], + bucket: process.env.ELECTRON_ENV ? s3Bucket[process.env.ELECTRON_ENV] : undefined, channel: 'latest', - path: s3Path[process.env.ELECTRON_ENV], + path: process.env.ELECTRON_ENV ? s3Path[process.env.ELECTRON_ENV] : undefined, }, ], files: ['dist', '!dist/www/{.well-known,favicon*}'], From 1dbe373fcadf2d9104e0232c463ed372b7cdafcc Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 09:03:41 +0100 Subject: [PATCH 032/173] migrate proxyConfig.js to TypeScript --- config/{proxyConfig.js => proxyConfig.ts} | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) rename config/{proxyConfig.js => proxyConfig.ts} (64%) diff --git a/config/proxyConfig.js b/config/proxyConfig.ts similarity index 64% rename from config/proxyConfig.js rename to config/proxyConfig.ts index fa09c436461f..0fecef28c1cf 100644 --- a/config/proxyConfig.js +++ b/config/proxyConfig.ts @@ -3,7 +3,14 @@ * We only specify for staging URLs as API requests are sent to the production * servers by default. */ -module.exports = { +type ProxyConfig = { + STAGING: string; + STAGING_SECURE: string; +}; + +const proxyConfig: ProxyConfig = { STAGING: '/staging/', STAGING_SECURE: '/staging-secure/', }; + +export default proxyConfig; From c6a484f6323a37553c4aad2c7df8d8fb9f7ee7d5 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 09:15:20 +0100 Subject: [PATCH 033/173] migrate webpack.common.js to TypeScript --- .../{webpack.common.js => webpack.common.ts} | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) rename config/webpack/{webpack.common.js => webpack.common.ts} (92%) diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.ts similarity index 92% rename from config/webpack/webpack.common.js rename to config/webpack/webpack.common.ts index 170198987793..71ba7e584fde 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.ts @@ -1,13 +1,13 @@ -const path = require('path'); -const fs = require('fs'); -const {IgnorePlugin, DefinePlugin, ProvidePlugin, EnvironmentPlugin} = require('webpack'); -const {CleanWebpackPlugin} = require('clean-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const CopyPlugin = require('copy-webpack-plugin'); -const dotenv = require('dotenv'); -const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); -const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin'); -const CustomVersionFilePlugin = require('./CustomVersionFilePlugin'); +import PreloadWebpackPlugin from '@vue/preload-webpack-plugin'; +import {CleanWebpackPlugin} from 'clean-webpack-plugin'; +import CopyPlugin from 'copy-webpack-plugin'; +import dotenv from 'dotenv'; +import fs from 'fs'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import path from 'path'; +import {DefinePlugin, EnvironmentPlugin, IgnorePlugin, ProvidePlugin} from 'webpack'; +import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; +import CustomVersionFilePlugin from './CustomVersionFilePlugin'; const includeModules = [ 'react-native-animatable', @@ -26,14 +26,14 @@ const includeModules = [ 'expo-av', ].join('|'); -const envToLogoSuffixMap = { +const envToLogoSuffixMap: Record = { production: '', staging: '-stg', dev: '-dev', adhoc: '-adhoc', }; -function mapEnvToLogoSuffix(envFile) { +function mapEnvToLogoSuffix(envFile: string): string { let env = envFile.split('.')[2]; if (typeof env === 'undefined') { env = 'dev'; @@ -43,10 +43,6 @@ function mapEnvToLogoSuffix(envFile) { /** * Get a production grade config for web or desktop - * @param {Object} env - * @param {String} env.envFile path to the env file to be used - * @param {'web'|'desktop'} env.platform - * @returns {Configuration} */ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ mode: 'production', @@ -276,4 +272,4 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ }, }); -module.exports = webpackConfig; +export default webpackConfig; From 866211c240e8fc27f1333f362633be44cc8dc231 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 09:15:38 +0100 Subject: [PATCH 034/173] migrate webpack.desktop.js to TypeScript --- config/webpack/{webpack.desktop.js => webpack.desktop.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename config/webpack/{webpack.desktop.js => webpack.desktop.ts} (95%) diff --git a/config/webpack/webpack.desktop.js b/config/webpack/webpack.desktop.ts similarity index 95% rename from config/webpack/webpack.desktop.js rename to config/webpack/webpack.desktop.ts index 2612e2b190fa..252538b8b072 100644 --- a/config/webpack/webpack.desktop.js +++ b/config/webpack/webpack.desktop.ts @@ -1,6 +1,6 @@ -const path = require('path'); -const webpack = require('webpack'); -const _ = require('underscore'); +import path from 'path'; +import _ from 'underscore'; +import webpack from 'webpack'; const desktopDependencies = require('../../desktop/package.json').dependencies; const getCommonConfig = require('./webpack.common'); From 847b4b7cf938ca19a232e2661e6207adade91d6f Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 09:16:07 +0100 Subject: [PATCH 035/173] start migrating webpack.dev.js to TypeScript --- .../{webpack.dev.js => webpack.dev.ts} | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) rename config/webpack/{webpack.dev.js => webpack.dev.ts} (85%) diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.ts similarity index 85% rename from config/webpack/webpack.dev.js rename to config/webpack/webpack.dev.ts index e28383eff557..9c5cb94aa86d 100644 --- a/config/webpack/webpack.dev.js +++ b/config/webpack/webpack.dev.ts @@ -1,18 +1,22 @@ -const path = require('path'); -const portfinder = require('portfinder'); -const {DefinePlugin} = require('webpack'); -const {merge} = require('webpack-merge'); -const {TimeAnalyticsPlugin} = require('time-analytics-webpack-plugin'); -const getCommonConfig = require('./webpack.common'); +/* eslint-disable @typescript-eslint/naming-convention */ +import path from 'path'; +import portfinder from 'portfinder'; +import {TimeAnalyticsPlugin} from 'time-analytics-webpack-plugin'; +import {DefinePlugin} from 'webpack'; +import {merge} from 'webpack-merge'; +import getCommonConfig from './webpack.common'; const BASE_PORT = 8082; +type EnvFile = Partial<{ + envFile: string; + platform: 'web' | 'desktop'; +}>; + /** * Configuration for the local dev server - * @param {Object} env - * @returns {Configuration} */ -module.exports = (env = {}) => +module.exports = (env: EnvFile = {}) => portfinder.getPortPromise({port: BASE_PORT}).then((port) => { // Check if the USE_WEB_PROXY variable has been provided // and rewrite any requests to the local proxy server From b29a9b336e80c3534adc54b7b01df40c1262f5fb Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 09:20:11 +0100 Subject: [PATCH 036/173] add module declaration for preload-webpack-plugin --- src/types/modules/preload-webpack-plugin.d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/types/modules/preload-webpack-plugin.d.ts diff --git a/src/types/modules/preload-webpack-plugin.d.ts b/src/types/modules/preload-webpack-plugin.d.ts new file mode 100644 index 000000000000..8f9d33a51080 --- /dev/null +++ b/src/types/modules/preload-webpack-plugin.d.ts @@ -0,0 +1,16 @@ +declare module '@vue/preload-webpack-plugin' { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Options { + rel: string; + as: string; + fileWhitelist: RegExp[]; + include: string; + } + + declare class PreloadWebpackPlugin { + constructor(options?: Options); + apply(compiler: Compiler): void; + } + + export default PreloadWebpackPlugin; +} From 7ad015da9a4a45231f168efeebbc0b795201d346 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 09:21:30 +0100 Subject: [PATCH 037/173] update scripts and documentation --- desktop/README.md | 2 +- scripts/build-desktop.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/README.md b/desktop/README.md index 77abff97a898..4ef763c6fedf 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -90,7 +90,7 @@ mc policy set public electron-builder/electron-builder Once you have Min.IO setup and running, the next step is to temporarily revert some changes from https://github.com/Expensify/App/commit/b640b3010fd7a40783d1c04faf4489836e98038d, specifically 1. Update the `desktop-build` command in package.json to add `--publish always` at the end -2. Update electronBuilder.config.js to replace the `publish` value with the following: +2. Update electronBuilder.config.ts to replace the `publish` value with the following: ``` publish: [{ provider: 's3', diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index 025559dc4671..2354ab9fdaa2 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -25,4 +25,4 @@ npx webpack --config config/webpack/webpack.desktop.js --env envFile=$ENV_FILE title "Building Desktop App Archive Using Electron" info "" shift 1 -npx electron-builder --config config/electronBuilder.config.js --publish always "$@" +npx electron-builder --config config/electronBuilder.config.ts --publish always "$@" From 0db97cfa2c0b4f31c02b1b8a05886516946172c3 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Mon, 4 Mar 2024 10:29:37 +0100 Subject: [PATCH 038/173] Remove unnecessary code --- src/components/MoneyRequestConfirmationList.js | 3 --- .../workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx | 2 +- .../workspace/workflows/WorkspaceWorkflowsApproverPage.tsx | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index dfe1d96b0e5d..b3a43cf583ea 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -378,14 +378,12 @@ function MoneyRequestConfirmationList(props) { title: translate('moneyRequestConfirmationList.paidBy'), data: [formattedPayeeOption], shouldShow: true, - indexOffset: 0, isDisabled: shouldDisablePaidBySection, }, { title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - indexOffset: 1, }, ); } else { @@ -397,7 +395,6 @@ function MoneyRequestConfirmationList(props) { title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, - indexOffset: 0, }); } return sections; diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx index 84d70e799c42..49c353f8b6aa 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx @@ -83,7 +83,7 @@ function WorkspaceAutoReportingMonthlyOffsetPage({policy}: WorkspaceAutoReportin /> 0, - indexOffset: 0, }); sectionsArray.push({ title: translate('common.all'), data: formattedPolicyMembers, shouldShow: true, - indexOffset: formattedApprover.length, }); return sectionsArray; From 21f632801ad6f5566c5619fe0ab8cdfe924b4fee Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 4 Mar 2024 16:32:52 +0100 Subject: [PATCH 039/173] fix webpack.common.ts --- config/webpack/webpack.common.ts | 1 + config/webpack/webpack.desktop.ts | 10 +++++----- config/webpack/webpack.dev.ts | 12 ++++++------ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 71ba7e584fde..b26a3a01e825 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import PreloadWebpackPlugin from '@vue/preload-webpack-plugin'; import {CleanWebpackPlugin} from 'clean-webpack-plugin'; import CopyPlugin from 'copy-webpack-plugin'; diff --git a/config/webpack/webpack.desktop.ts b/config/webpack/webpack.desktop.ts index 252538b8b072..397fb6f7a3fd 100644 --- a/config/webpack/webpack.desktop.ts +++ b/config/webpack/webpack.desktop.ts @@ -1,6 +1,6 @@ -import path from 'path'; -import _ from 'underscore'; -import webpack from 'webpack'; +const path = require('path'); +const _ = require('underscore'); +const webpack = require('webpack'); const desktopDependencies = require('../../desktop/package.json').dependencies; const getCommonConfig = require('./webpack.common'); @@ -10,8 +10,8 @@ const getCommonConfig = require('./webpack.common'); * 1. electron-main - the core that serves the app content * 2. web - the app content that would be rendered in electron * Everything is placed in desktop/dist and ready for packaging - * @param {Object} env - * @returns {webpack.Configuration[]} + * @param env + * @returns */ module.exports = (env) => { const rendererConfig = getCommonConfig({...env, platform: 'desktop'}); diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 9c5cb94aa86d..0d9a44c0bf20 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import path from 'path'; -import portfinder from 'portfinder'; -import {TimeAnalyticsPlugin} from 'time-analytics-webpack-plugin'; -import {DefinePlugin} from 'webpack'; -import {merge} from 'webpack-merge'; -import getCommonConfig from './webpack.common'; +const path = require('path'); +const portfinder = require('portfinder'); +const {TimeAnalyticsPlugin} = require('time-analytics-webpack-plugin'); +const {DefinePlugin} = require('webpack'); +const {merge} = require('webpack-merge'); +const getCommonConfig = require('./webpack.common'); const BASE_PORT = 8082; From 59522b781097467f4003a3af1fec52c77ea511bb Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 5 Mar 2024 10:07:15 +0100 Subject: [PATCH 040/173] update imports in webpack.dev.ts --- config/webpack/webpack.dev.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 0d9a44c0bf20..45e79a3bf578 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import portfinder = require('portfinder'); +import TimeAnalyticsWebpackPlugin = require('time-analytics-webpack-plugin'); + +const {TimeAnalyticsPlugin} = TimeAnalyticsWebpackPlugin; const path = require('path'); -const portfinder = require('portfinder'); -const {TimeAnalyticsPlugin} = require('time-analytics-webpack-plugin'); const {DefinePlugin} = require('webpack'); const {merge} = require('webpack-merge'); const getCommonConfig = require('./webpack.common'); @@ -16,7 +18,7 @@ type EnvFile = Partial<{ /** * Configuration for the local dev server */ -module.exports = (env: EnvFile = {}) => +const getConfig = (env: EnvFile = {}) => portfinder.getPortPromise({port: BASE_PORT}).then((port) => { // Check if the USE_WEB_PROXY variable has been provided // and rewrite any requests to the local proxy server @@ -64,7 +66,7 @@ module.exports = (env: EnvFile = {}) => ], cache: { type: 'filesystem', - name: env.platform || 'default', + name: env.platform ?? 'default', buildDependencies: { // By default, webpack and loaders are build dependencies // This (also) makes all dependencies of this config file - build dependencies @@ -82,3 +84,5 @@ module.exports = (env: EnvFile = {}) => return TimeAnalyticsPlugin.wrap(config); }); + +export default getConfig; From 1acf6b24430446f1820f47854a036b94e2fb998b Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 5 Mar 2024 11:04:44 +0100 Subject: [PATCH 041/173] Fix comments --- src/components/OptionsList/BaseOptionsList.tsx | 12 ++++++------ src/components/OptionsList/types.ts | 14 +++++++++----- src/components/SelectionList/BaseSelectionList.tsx | 9 ++++----- src/components/SelectionList/types.ts | 11 +++++++---- src/libs/getSectionsWithIndexOffset.ts | 6 ++++-- src/pages/EditReportFieldDropdownPage.tsx | 1 + 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 50e0ce31fcc2..436f4c147931 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -14,7 +14,7 @@ import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {BaseOptionListProps, OptionsList, OptionsListData, Section} from './types'; +import type {BaseOptionListProps, OptionsList, OptionsListData, OptionsListDataWithIndexOffset, SectionWithIndexOffset} from './types'; function BaseOptionsList( { @@ -136,7 +136,7 @@ function BaseOptionsList( * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] */ // eslint-disable-next-line @typescript-eslint/naming-convention - const getItemLayout = (_data: OptionsListData[] | null, flatDataArrayIndex: number) => { + const getItemLayout = (_data: OptionsListDataWithIndexOffset[] | null, flatDataArrayIndex: number) => { if (!flattenedData.current[flatDataArrayIndex]) { flattenedData.current = buildFlatSectionArray(); } @@ -164,7 +164,7 @@ function BaseOptionsList( * @return {Component} */ - const renderItem: SectionListRenderItem = ({item, index, section}) => { + const renderItem: SectionListRenderItem = ({item, index, section}) => { const isItemDisabled = isDisabled || !!section.isDisabled || !!item.isDisabled; const isSelected = selectedOptions?.some((option) => { if (option.keyForList && option.keyForList === item.keyForList) { @@ -184,7 +184,7 @@ function BaseOptionsList( option={item} showTitleTooltip={showTitleTooltip} hoverStyle={optionHoveredStyle} - optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + (section.indexOffset ?? 0)} + optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset} onSelectRow={onSelectRow} isSelected={isSelected} showSelectedState={canSelectMultipleOptions} @@ -205,7 +205,7 @@ function BaseOptionsList( /** * Function which renders a section header component */ - const renderSectionHeader = ({section: {title, shouldShow}}: {section: OptionsListData}) => { + const renderSectionHeader = ({section: {title, shouldShow}}: {section: OptionsListDataWithIndexOffset}) => { if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { return ; } @@ -238,7 +238,7 @@ function BaseOptionsList( {headerMessage} ) : null} - + ref={ref} style={listStyles} indicatorStyle="white" diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts index c8c117d800e4..9d7ed8ecf362 100644 --- a/src/components/OptionsList/types.ts +++ b/src/components/OptionsList/types.ts @@ -2,16 +2,15 @@ import type {RefObject} from 'react'; import type {SectionList, SectionListData, StyleProp, View, ViewStyle} from 'react-native'; import type {OptionData} from '@libs/ReportUtils'; -type OptionsList = SectionList; type OptionsListData = SectionListData; +type OptionsListDataWithIndexOffset = SectionListData; + +type OptionsList = SectionList; type Section = { /** Title of the section */ title: string; - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset?: number; - /** Array of options */ data: OptionData[]; @@ -22,6 +21,11 @@ type Section = { isDisabled?: boolean; }; +type SectionWithIndexOffset = Section & { + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset: number; +}; + type OptionsListProps = { /** option flexStyle for the options list container */ listContainerStyles?: StyleProp; @@ -134,4 +138,4 @@ type BaseOptionListProps = OptionsListProps & { listStyles?: StyleProp; }; -export type {OptionsListProps, BaseOptionListProps, Section, OptionsList, OptionsListData}; +export type {OptionsListProps, BaseOptionListProps, Section, OptionsList, OptionsListData, SectionWithIndexOffset, OptionsListDataWithIndexOffset}; diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 920716b230fc..0adb23115b15 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -22,7 +22,7 @@ import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, ListItem, Section, SectionListDataType} from './types'; +import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, ListItem, SectionListDataType, SectionWithIndexOffset} from './types'; function BaseSelectionList( { @@ -70,7 +70,7 @@ function BaseSelectionList( ) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const listRef = useRef>>(null); + const listRef = useRef>>(null); const textInputRef = useRef(null); const focusTimeoutRef = useRef(null); const shouldShowTextInput = !!textInputLabel; @@ -276,9 +276,8 @@ function BaseSelectionList( ); }; - const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { - const indexOffset = section.indexOffset ? section.indexOffset : 0; - const normalizedIndex = index + indexOffset; + const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { + const normalizedIndex = index + section.indexOffset; const isDisabled = !!section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && focusedIndex === normalizedIndex; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 005a8ab21cc1..440ea31a97e8 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -143,9 +143,6 @@ type Section = { /** Title of the section */ title?: string; - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset?: number; - /** Array of options */ data?: TItem[]; @@ -156,6 +153,11 @@ type Section = { shouldShow?: boolean; }; +type SectionWithIndexOffset = Section & { + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset: number; +}; + type BaseSelectionListProps = Partial & { /** Sections for the section list */ sections: Array>>; @@ -293,12 +295,13 @@ type FlattenedSectionsReturn = { type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; -type SectionListDataType = SectionListData>; +type SectionListDataType = SectionListData>; export type { BaseSelectionListProps, CommonListItemProps, Section, + SectionWithIndexOffset, BaseListItemProps, UserListItemProps, RadioListItemProps, diff --git a/src/libs/getSectionsWithIndexOffset.ts b/src/libs/getSectionsWithIndexOffset.ts index e6c0820374e8..7de78d048a4d 100644 --- a/src/libs/getSectionsWithIndexOffset.ts +++ b/src/libs/getSectionsWithIndexOffset.ts @@ -1,7 +1,9 @@ import type {SectionListData} from 'react-native'; -/** Returns a list of sections with IndexOffset */ -export default function getSectionsWithIndexOffset(sections: Array>) { +/** + * Returns a list of sections with indexOffset + */ +export default function getSectionsWithIndexOffset(sections: Array>): Array> { return sections.map((section, index) => { const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + (curr.data?.length ?? 0), 0); return {...section, indexOffset}; diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 1ad63bb2bf2f..e8f1d97494af 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -92,6 +92,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, textInputLabel={translate('common.search')} boldStyle sections={sections} + // Focus the first option when searching focusedIndex={0} value={searchValue} onSelectRow={(option: Record) => onSubmit({[fieldID]: option.text})} From 7d67f31369e367a20f5a2135269ad2b668e5c383 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 5 Mar 2024 11:11:28 +0100 Subject: [PATCH 042/173] Remove unnecessary code --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index c82740eff361..7d9ceac74203 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -113,7 +113,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { {tagList.length ? ( {}} onSelectAll={toggleAllTags} From 0f215046bb6bc069d00a4287ed3baf2d1044588d Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Tue, 5 Mar 2024 23:04:04 -0800 Subject: [PATCH 043/173] update utils files to no longer reference report.transactionThreadReportID --- src/libs/ReportActionsUtils.ts | 45 ++++++++++++++++++++-------------- src/libs/ReportUtils.ts | 4 +-- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 47323ad8cd56..d0682861dd50 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -205,6 +205,31 @@ function isTransactionThread(parentReportAction: OnyxEntry): boole ); } +function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxCollection | null): string { + const moneyRequestReportActions = reportActions && !_.isEmpty(reportActions) ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] : allReportActions?.[reportID]; + const moneyRequestReportActionsArray = Object.values(moneyRequestReportActions ?? {}); + + if (!moneyRequestReportActionsArray.length) { + return '0'; + } + + // Get all IOU report actions for the report. + const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT]; + const iouRequestActions = moneyRequestReportActionsArray.filter((action) => + action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU + && (iouRequestTypes.includes(action.originalMessage.type) ?? []) + && action.childReportID + ); + + // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report and we don't want to return the transactionThreadReportActions + if (!iouRequestActions.length || iouRequestActions.length > 1) { + return '0'; + } + + // Ensure we have a childReportID associated with the IOU report action + return String(iouRequestActions[0].childReportID); +} + /** * Sort an array of reportActions by their created timestamp first, and reportActionID second * This gives us a stable order even in the case of multiple reportActions created on the same millisecond @@ -711,24 +736,6 @@ function getAllReportActions(reportID: string): ReportActions { return allReportActions?.[reportID] ?? {}; } -/** - * Gets an array of IOU report actions - */ -function getIOUReportActions(reportID: string): ReportAction[] | null { - const reportActions = Object.values(getAllReportActions(reportID)); - if (!reportActions.length) { - return null; - } - - const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT]; - const iouRequestActions = reportActions?.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && iouRequestTypes.includes(action.originalMessage.type)) ?? []; - - if (!iouRequestActions.length) { - return null; - } - return iouRequestActions; -} - /** * Check whether a report action is an attachment (a file, such as an image or a zip). * @@ -919,8 +926,8 @@ function isCurrentActionUnread(report: Report | EmptyObject, reportAction: Repor export { extractLinksFromMessageHtml, + getOneTransactionThreadReportID, getAllReportActions, - getIOUReportActions, getIOUReportIDFromReportActionPreview, getLastClosedReportAction, getLastVisibleAction, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b3d9da91c616..3963eb90127f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1206,7 +1206,7 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { * Checks if a report has only one transaction associated with it */ function isOneTransactionReport(report: OnyxEntry): boolean { - return report?.transactionThreadReportID !== undefined; + return ReportActionsUtils.getOneTransactionThreadReportID(report?.reportID ?? '0', null) !== '0' } /** @@ -1217,7 +1217,7 @@ function isOneTransactionThread(reportID: string, parentReport: OnyxEntry Date: Tue, 5 Mar 2024 23:05:11 -0800 Subject: [PATCH 044/173] use general report and reportActions onyx keys instead of relying on report.transactionThreadReportID --- src/pages/home/ReportScreen.js | 25 +++++++++----- src/pages/home/report/ReportActionItem.js | 40 +++++++++++++++-------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d0d74274a14e..b874fb7ae393 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -65,6 +65,9 @@ const propTypes = { /** The report metadata loading states */ reportMetadata: reportMetadataPropTypes, + /** All the report actions stored in Onyx */ + allReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** All the report actions for this report */ reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), @@ -107,6 +110,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, reportActions: [], + allReportActions: {}, transactionThreadReportActions: [], parentReportAction: {}, report: {}, @@ -146,8 +150,8 @@ function ReportScreen({ route, report: reportProp, reportMetadata, + allReportActions, reportActions, - transactionThreadReportActions, parentReportAction, accountManagerReportID, markReadyForHydration, @@ -212,7 +216,6 @@ function ReportScreen({ policyName: reportProp.policyName, isOptimisticReport: reportProp.isOptimisticReport, lastMentionedTime: reportProp.lastMentionedTime, - transactionThreadReportID: reportProp.transactionThreadReportID, }), [ reportProp.lastReadTime, @@ -250,7 +253,6 @@ function ReportScreen({ reportProp.policyName, reportProp.isOptimisticReport, reportProp.lastMentionedTime, - reportProp.transactionThreadReportID, ], ); @@ -286,6 +288,13 @@ function ReportScreen({ const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, allReportActions); + const transactionThreadReportActions = useMemo(() => { + if (transactionThreadReportID) { + return null; + } + return ReportActionsUtils.getSortedReportActionsForDisplay(Object.values(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportReportID}`])); + }, [allReportActions, transactionThreadReportID]); useEffect(() => { if (!report.reportID || shouldHideReport) { @@ -617,16 +626,14 @@ export default compose( isSidebarLoaded: { key: ONYXKEYS.IS_SIDEBAR_LOADED, }, + allReportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, reportActions: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, - transactionThreadReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? (report.transactionThreadReportID || '0') : '0'}`, - canEvict: false, - selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), - }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, allowStaleData: true, @@ -678,8 +685,8 @@ export default compose( ReportScreen, (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && + _.isEqual(prevProps.allReportActions, nextProps.allReportActions) && _.isEqual(prevProps.reportActions, nextProps.reportActions) && - _.isEqual(prevProps.transactionThreadReportActions, nextProps.transactionThreadReportActions) && _.isEqual(prevProps.reportMetadata, nextProps.reportMetadata) && prevProps.isComposerFullSize === nextProps.isComposerFullSize && _.isEqual(prevProps.betas, nextProps.betas) && diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 3baba4e2ea22..57794494d34f 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -111,12 +111,15 @@ const propTypes = { emojiReactions: EmojiReactionsPropTypes, + /** All reportActions shared with the user */ + reportActions: PropTypes.objectOf(reportActionPropTypes), + + /** All reports shared with the user */ + reports: PropTypes.objectOf(reportPropTypes), + /** IOU report for this action, if any */ iouReport: reportPropTypes, - /** Single transaction thread associated with the report, if any */ - transactionThreadReport: reportPropTypes, - /** Flag to show, hide the thread divider line */ shouldHideThreadDividerLine: PropTypes.bool, @@ -135,8 +138,9 @@ const defaultProps = { preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, emojiReactions: {}, shouldShowSubscriptAvatar: false, + reportActions: {}, + reports: {}, iouReport: undefined, - transactionThreadReport: undefined, shouldHideThreadDividerLine: false, userWallet: {}, parentReportActions: {}, @@ -160,6 +164,13 @@ function ReportActionItem(props) { const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID; + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(props.report.reportID, props.allReportActions); + const transactionThreadReport = useMemo(() => { + if (transactionThreadReportID) { + return null; + } + return props.reports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportReportID}`]; + }, [props.reports, transactionThreadReportID]); const reportScrollManager = useReportScrollManager(); @@ -701,9 +712,9 @@ function ReportActionItem(props) { if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { return ( - {props.transactionThreadReport && !isEmptyObject(props.transactionThreadReport) ? ( + {transactionThreadReport && !isEmptyObject(transactionThreadReport) ? ( <> - {props.transactionThreadReport.currency !== props.report.currency && ( + {transactionThreadReport.currency !== props.report.currency && ( @@ -881,6 +892,12 @@ export default compose( key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, }, + reportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, iouReport: { key: ({action}) => { const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); @@ -888,10 +905,6 @@ export default compose( }, initialValue: {}, }, - transactionThreadReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.transactionThreadReportID || 0}`, - initialValue: {}, - }, policyReportFields: { key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), initialValue: [], @@ -921,13 +934,14 @@ export default compose( prevProps.draftMessage === nextProps.draftMessage && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && + _.isEqual(prevProps.reportActions, nextProps.reportActions) && + _.isEqual(prevProps.reports, nextProps.reports) && _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && _.isEqual(prevProps.action, nextProps.action) && _.isEqual(prevProps.iouReport, nextProps.iouReport) && _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) && - _.isEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') && lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') && lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') && From 1819da7109e6cff37c0e53db44eec79e8c7aa148 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 09:07:37 +0100 Subject: [PATCH 045/173] update scripts in package.json, install webpack types --- package-lock.json | 100 +++++++++++++++++++++++++++++++++++++--------- package.json | 11 ++--- 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31933d6f3596..3f9d80e1417d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -184,6 +184,7 @@ "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", "@types/underscore": "^1.11.5", + "@types/webpack": "^5.28.5", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", @@ -13261,6 +13262,20 @@ "integrity": "sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg==", "dev": true }, + "node_modules/@storybook/builder-webpack4/node_modules/@types/webpack": { + "version": "4.41.38", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz", + "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/@storybook/builder-webpack4/node_modules/@webassemblyjs/helper-buffer": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", @@ -16068,6 +16083,20 @@ "integrity": "sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg==", "dev": true }, + "node_modules/@storybook/core-server/node_modules/@types/webpack": { + "version": "4.41.38", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz", + "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/@storybook/core-server/node_modules/@webassemblyjs/helper-buffer": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", @@ -17167,6 +17196,20 @@ "integrity": "sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg==", "dev": true }, + "node_modules/@storybook/manager-webpack4/node_modules/@types/webpack": { + "version": "4.41.38", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz", + "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/@storybook/manager-webpack4/node_modules/@webassemblyjs/helper-buffer": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", @@ -21110,11 +21153,10 @@ } }, "node_modules/@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true, - "license": "MIT" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==", + "dev": true }, "node_modules/@types/stack-utils": { "version": "2.0.1", @@ -21136,9 +21178,10 @@ "license": "MIT" }, "node_modules/@types/uglify-js": { - "version": "3.17.0", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", "dev": true, - "license": "MIT", "dependencies": { "source-map": "^0.6.1" } @@ -21178,16 +21221,14 @@ "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" }, "node_modules/@types/webpack": { - "version": "4.41.32", + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" + "tapable": "^2.2.0", + "webpack": "^5" } }, "node_modules/@types/webpack-bundle-analyzer": { @@ -21216,11 +21257,10 @@ "license": "MIT" }, "node_modules/@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "@types/source-list-map": "*", @@ -21232,11 +21272,19 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, + "node_modules/@types/webpack/node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@types/ws": { "version": "8.5.3", "dev": true, @@ -25982,6 +26030,20 @@ "webpack": "*" } }, + "node_modules/clean-webpack-plugin/node_modules/@types/webpack": { + "version": "4.41.38", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz", + "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", diff --git a/package.json b/package.json index 614244de50bb..26969642c787 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "start": "npx react-native start", "web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", "web-proxy": "ts-node web/proxy.js", - "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.js", - "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", - "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", - "build-adhoc": "webpack --config config/webpack/webpack.common.js --env envFile=.env.adhoc", + "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.ts", + "build": "webpack --config config/webpack/webpack.common.ts --env envFile=.env.production", + "build-staging": "webpack --config config/webpack/webpack.common.ts --env envFile=.env.staging", + "build-adhoc": "webpack --config config/webpack/webpack.common.ts --env envFile=.env.adhoc", "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.js", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", @@ -47,7 +47,7 @@ "storybook-build-staging": "ENV=staging build-storybook -o dist/docs", "gh-actions-build": "./.github/scripts/buildActions.sh", "gh-actions-validate": "./.github/scripts/validateActionsAndWorkflows.sh", - "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", + "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.ts --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", "test:e2e": "ts-node tests/e2e/testRunner.js --config ./config.local.ts", @@ -233,6 +233,7 @@ "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", "@types/underscore": "^1.11.5", + "@types/webpack": "^5.28.5", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", From 05b5c439b3eaca8fd38c6cafdb8e117be4726fce Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 09:20:43 +0100 Subject: [PATCH 046/173] update file extensions in documentation --- README.md | 2 +- desktop/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72736b3fedb7..3f22cff7cb63 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ If you're using another operating system, you will need to ensure `mkcert` is in ## Running the web app 🕸 * To run the **development web app**: `npm run web` -* Changes applied to Javascript will be applied automatically via WebPack as configured in `webpack.dev.js` +* Changes applied to Javascript will be applied automatically via WebPack as configured in `webpack.dev.ts` ## Running the iOS app 📱 For an M1 Mac, read this [SO](https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1) for installing cocoapods. diff --git a/desktop/README.md b/desktop/README.md index 4ef763c6fedf..bd68ec571659 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -129,7 +129,7 @@ Once the command finishes, revert the version update in `package.json`, remove ` To avoid bundling unnecessary `node_modules` we use a [2 package structure](https://www.electron.build/tutorials/two-package-structure) The root [package.json](../package.json) serves for `devDependencies` and shared (renderer) `dependencies` The [desktop/package.json](./package.json) serves for desktop (electron-main) specific dependencies -We use Webpack with a [desktop specific config](../config/webpack/webpack.desktop.js) to bundle our js code +We use Webpack with a [desktop specific config](../config/webpack/webpack.desktop.ts) to bundle our js code Half of the config takes care of packaging root package dependencies - everything related to rendering App in the Electron window. Packaged under `dist/www` The other half is about bundling the `main.js` script which initializes Electron and renders `www` From 8bcb07885539f46c998659ba88e1049f92ad1e88 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 09:29:03 +0100 Subject: [PATCH 047/173] update tsconfig --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 30708f63d12b..59f095d3f752 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "skipLibCheck": true, "incremental": true, "baseUrl": ".", From 56e5b173a37a7087e65595f589f83b89ce450887 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 09:30:26 +0100 Subject: [PATCH 048/173] update file extensions in documentation --- contributingGuides/APPLE_GOOGLE_SIGNIN.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index 4bb86e31b486..e62d78e020af 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -167,7 +167,7 @@ After you've set ngrok up to be able to run on your machine (requires configurin ngrok http 8082 --host-header="dev.new.expensify.com:8082" --subdomain=mysubdomain ``` -The `--host-header` flag is there to avoid webpack errors with header validation. In addition, add `allowedHosts: 'all'` to the dev server config in `webpack.dev.js`: +The `--host-header` flag is there to avoid webpack errors with header validation. In addition, add `allowedHosts: 'all'` to the dev server config in `webpack.dev.ts`: ```js devServer: { @@ -265,13 +265,13 @@ Google allows the web app to be hosted at localhost, but according to the current Google console configuration for the Expensify client ID, it must be hosted on port 8082. -Also note that you'll need to update the webpack.dev.js config to change `host` from `dev.new.expensify.com` to `localhost` and server type from `https` to `http`. The reason for this is that Google Sign In allows localhost, but `dev.new.expensify.com` is not a registered Google Sign In domain. +Also note that you'll need to update the webpack.dev.ts config to change `host` from `dev.new.expensify.com` to `localhost` and server type from `https` to `http`. The reason for this is that Google Sign In allows localhost, but `dev.new.expensify.com` is not a registered Google Sign In domain. ```diff -diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js +diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index e28383eff5..b14f6f34aa 100644 ---- a/config/webpack/webpack.dev.js -+++ b/config/webpack/webpack.dev.js +--- a/config/webpack/webpack.dev.ts ++++ b/config/webpack/webpack.dev.ts @@ -44,9 +44,9 @@ module.exports = (env = {}) => ...proxySettings, historyApiFallback: true, From b80efcca9aea1c1c2299cb3893ecf91780561e95 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 09:52:40 +0100 Subject: [PATCH 049/173] update file extension in webpack.config.js --- .storybook/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 204f70344b18..07b57c687daf 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -28,7 +28,7 @@ module.exports = ({config}) => { '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), - // Module alias support for storybook files, coping from `webpack.common.js` + // Module alias support for storybook files, coping from `webpack.common.ts` ...custom.resolve.alias, }; From 7649b0298aad7ccd0dc02215445be820ee095d64 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 10:05:48 +0100 Subject: [PATCH 050/173] migrate webpack.common.js to TypeScript --- config/webpack/webpack.common.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index b26a3a01e825..2916064bf2a4 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -1,14 +1,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import PreloadWebpackPlugin from '@vue/preload-webpack-plugin'; -import {CleanWebpackPlugin} from 'clean-webpack-plugin'; -import CopyPlugin from 'copy-webpack-plugin'; -import dotenv from 'dotenv'; -import fs from 'fs'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; -import path from 'path'; -import {DefinePlugin, EnvironmentPlugin, IgnorePlugin, ProvidePlugin} from 'webpack'; -import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; -import CustomVersionFilePlugin from './CustomVersionFilePlugin'; +import type {Configuration} from 'webpack'; + +const {CleanWebpackPlugin} = require('clean-webpack-plugin'); +const CopyPlugin = require('copy-webpack-plugin'); +const dotenv = require('dotenv'); +const fs = require('fs'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const path = require('path'); +const {DefinePlugin, EnvironmentPlugin, IgnorePlugin, ProvidePlugin} = require('webpack'); +const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); +const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin'); +const CustomVersionFilePlugin = require('./CustomVersionFilePlugin'); const includeModules = [ 'react-native-animatable', @@ -45,7 +47,7 @@ function mapEnvToLogoSuffix(envFile: string): string { /** * Get a production grade config for web or desktop */ -const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ +const getCommonConfig = ({envFile = '.env', platform = 'web'}): Configuration => ({ mode: 'production', devtool: 'source-map', entry: { @@ -273,4 +275,4 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ }, }); -export default webpackConfig; +export default getCommonConfig; From a0ba0148abbbb681ba8e46c125b26661b694c59b Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 10:08:48 +0100 Subject: [PATCH 051/173] migrate webpack.desktop.js to TypeScript --- config/webpack/webpack.desktop.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/config/webpack/webpack.desktop.ts b/config/webpack/webpack.desktop.ts index 397fb6f7a3fd..9c28afd614d5 100644 --- a/config/webpack/webpack.desktop.ts +++ b/config/webpack/webpack.desktop.ts @@ -1,27 +1,30 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import getCommonConfig from './webpack.common'; +import type {EnvFile} from './webpack.dev'; + const path = require('path'); -const _ = require('underscore'); const webpack = require('webpack'); const desktopDependencies = require('../../desktop/package.json').dependencies; -const getCommonConfig = require('./webpack.common'); /** * Desktop creates 2 configurations in parallel * 1. electron-main - the core that serves the app content * 2. web - the app content that would be rendered in electron * Everything is placed in desktop/dist and ready for packaging - * @param env - * @returns */ -module.exports = (env) => { +const getConfig = (env: EnvFile = {}) => { const rendererConfig = getCommonConfig({...env, platform: 'desktop'}); const outputPath = path.resolve(__dirname, '../../desktop/dist'); rendererConfig.name = 'renderer'; - rendererConfig.output.path = path.join(outputPath, 'www'); + if (rendererConfig.output) { + rendererConfig.output.path = path.join(outputPath, 'www'); + } // Expose react-native-config to desktop-main - const definePlugin = _.find(rendererConfig.plugins, (plugin) => plugin.constructor === webpack.DefinePlugin); + // const definePlugin = _.find(rendererConfig.plugins, (plugin) => plugin.constructor === webpack.DefinePlugin); + const definePlugin = rendererConfig.plugins?.find((plugin) => plugin?.constructor === webpack.DefinePlugin); const mainProcessConfig = { mode: 'production', @@ -38,7 +41,7 @@ module.exports = (env) => { }, resolve: rendererConfig.resolve, plugins: [definePlugin], - externals: [..._.keys(desktopDependencies), 'fsevents'], + externals: [...Object.keys(desktopDependencies), 'fsevents'], node: { /** * Disables webpack processing of __dirname and __filename, so it works like in node @@ -60,3 +63,5 @@ module.exports = (env) => { return [mainProcessConfig, rendererConfig]; }; + +export default getConfig; From 512cf42d93b4c7ddea593012515db28c7abbdf5c Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 10:09:22 +0100 Subject: [PATCH 052/173] migrate webpack.dev.js to TypeScript --- config/webpack/webpack.dev.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 45e79a3bf578..56dc735edd86 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -1,12 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import type webpack from 'webpack'; + import portfinder = require('portfinder'); import TimeAnalyticsWebpackPlugin = require('time-analytics-webpack-plugin'); +import getCommonConfig from './webpack.common'; const {TimeAnalyticsPlugin} = TimeAnalyticsWebpackPlugin; const path = require('path'); const {DefinePlugin} = require('webpack'); const {merge} = require('webpack-merge'); -const getCommonConfig = require('./webpack.common'); const BASE_PORT = 8082; @@ -18,7 +20,7 @@ type EnvFile = Partial<{ /** * Configuration for the local dev server */ -const getConfig = (env: EnvFile = {}) => +const getConfig = (env: EnvFile = {}): Promise => portfinder.getPortPromise({port: BASE_PORT}).then((port) => { // Check if the USE_WEB_PROXY variable has been provided // and rewrite any requests to the local proxy server @@ -86,3 +88,4 @@ const getConfig = (env: EnvFile = {}) => }); export default getConfig; +export type {EnvFile}; From 5ef518ace9b9fa291cd809ea9d86dc1482be6cc6 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 6 Mar 2024 10:14:50 +0100 Subject: [PATCH 053/173] update file extensions in scripts --- desktop/start.js | 4 ++-- scripts/build-desktop.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/start.js b/desktop/start.js index 05a1b031350d..0802dabdda2f 100644 --- a/desktop/start.js +++ b/desktop/start.js @@ -10,8 +10,8 @@ portfinder port: basePort, }) .then((port) => { - const devServer = `webpack-dev-server --config config/webpack/webpack.dev.js --port ${port} --env platform=desktop`; - const buildMain = 'webpack watch --config config/webpack/webpack.desktop.js --config-name desktop-main --mode=development'; + const devServer = `webpack-dev-server --config config/webpack/webpack.dev.ts --port ${port} --env platform=desktop`; + const buildMain = 'webpack watch --config config/webpack/webpack.desktop.ts --config-name desktop-main --mode=development'; const env = { PORT: port, diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index 2354ab9fdaa2..0c8a19c592e6 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -20,7 +20,7 @@ title "Bundling Desktop js Bundle Using Webpack" info " • ELECTRON_ENV: $ELECTRON_ENV" info " • ENV file: $ENV_FILE" info "" -npx webpack --config config/webpack/webpack.desktop.js --env envFile=$ENV_FILE +npx webpack --config config/webpack/webpack.desktop.ts --env envFile=$ENV_FILE title "Building Desktop App Archive Using Electron" info "" From c941d2388e132ad241836c2a73ed2761455a7364 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Wed, 6 Mar 2024 23:07:19 -0800 Subject: [PATCH 054/173] update getOneTransactionReportID to take in only reportActions modify ReportUtils methods to send across reportActions --- src/libs/ReportActionsUtils.ts | 9 ++++----- src/libs/ReportUtils.ts | 19 ++++++++----------- src/pages/home/ReportScreen.js | 2 +- src/pages/home/report/ReportActionItem.js | 21 +++++++++++---------- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 227a67d06c48..08a97728eefe 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -205,17 +205,16 @@ function isTransactionThread(parentReportAction: OnyxEntry): boole ); } -function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxCollection | null): string { - const moneyRequestReportActions = reportActions && !_.isEmpty(reportActions) ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] : allReportActions?.[reportID]; - const moneyRequestReportActionsArray = Object.values(moneyRequestReportActions ?? {}); +function getOneTransactionThreadReportID(reportActions: ReportActions): string { + const reportActionsArray = Object.values(reportActions ?? {}); - if (!moneyRequestReportActionsArray.length) { + if (!reportActionsArray.length) { return '0'; } // Get all IOU report actions for the report. const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT]; - const iouRequestActions = moneyRequestReportActionsArray.filter((action) => + const iouRequestActions = reportActionsArray.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (iouRequestTypes.includes(action.originalMessage.type) ?? []) && action.childReportID diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6e9554c728fd..393dfec86f18 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1213,19 +1213,17 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { /** * Checks if a report has only one transaction associated with it */ -function isOneTransactionReport(report: OnyxEntry): boolean { - return ReportActionsUtils.getOneTransactionThreadReportID(report?.reportID ?? '0', null) !== '0' +function isOneTransactionReport(reportID: string): boolean { + const reportActions = ReportActionsUtils.getAllReportActions(reportID); + return ReportActionsUtils.getOneTransactionThreadReportID(reportActions) !== '0' } /** * Checks if a report is a transaction thread associated with a report that has only one transaction */ -function isOneTransactionThread(reportID: string, parentReport: OnyxEntry | EmptyObject): boolean { - if (isEmptyObject(parentReport)) { - return false; - } - - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReport?.reportID ?? '0', null); +function isOneTransactionThread(reportID: string, parentReportID: string): boolean { + const parentReportActions = ReportActionsUtils.getAllReportActions(parentReportID); + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportActions); return reportID === transactionThreadReportID; } @@ -1656,7 +1654,7 @@ function getIcons( const isManager = currentUserAccountID === report?.managerID; // For one transaction IOUs, display a simplified report icon - if (isOneTransactionReport(report)) { + if (isOneTransactionReport(report?.reportID ?? '0')) { return [ownerIcon]; } @@ -4039,8 +4037,7 @@ function shouldReportBeInOptionList({ } // If this is a transaction thread associated with a report that only has one transaction, omit it - const parentReport = getParentReport(report); - if (isOneTransactionThread(report.reportID, parentReport)) { + if (isOneTransactionThread(report.reportID, report.parentReportID ?? '0')) { return false; } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 527045e8546e..22858982544d 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -288,7 +288,7 @@ function ReportScreen({ const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; const isTopMostReportId = currentReportID === getReportID(route); const didSubscribeToReportLeavingEvents = useRef(false); - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, allReportActions); + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]); const transactionThreadReportActions = useMemo(() => { if (transactionThreadReportID) { return null; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 00289a07f448..9d0b555bba33 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -111,9 +111,6 @@ const propTypes = { emojiReactions: EmojiReactionsPropTypes, - /** All reportActions shared with the user */ - reportActions: PropTypes.objectOf(reportActionPropTypes), - /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), @@ -129,6 +126,9 @@ const propTypes = { /** All the report actions belonging to the report's parent */ parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** All the report actions belonging to the current report */ + reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** Callback to be called on onPress */ onPress: PropTypes.func, }; @@ -138,12 +138,12 @@ const defaultProps = { preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, emojiReactions: {}, shouldShowSubscriptAvatar: false, - reportActions: {}, reports: {}, iouReport: undefined, shouldHideThreadDividerLine: false, userWallet: {}, parentReportActions: {}, + reportActions: {}, onPress: undefined, }; @@ -166,7 +166,7 @@ function ReportActionItem(props) { const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID; - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(props.report.reportID, props.allReportActions); + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(props.reportActions); const transactionThreadReport = useMemo(() => { if (transactionThreadReportID) { return null; @@ -888,9 +888,6 @@ export default compose( key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, }, - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, reports: { key: ONYXKEYS.COLLECTION.REPORT, }, @@ -920,6 +917,10 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || 0}`, canEvict: false, }, + reportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID || 0}`, + canEvict: false, + }, }), )( memo(ReportActionItem, (prevProps, nextProps) => { @@ -930,7 +931,6 @@ export default compose( prevProps.draftMessage === nextProps.draftMessage && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && - _.isEqual(prevProps.reportActions, nextProps.reportActions) && _.isEqual(prevProps.reports, nextProps.reports) && _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && _.isEqual(prevProps.action, nextProps.action) && @@ -957,7 +957,8 @@ export default compose( _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) && _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) && _.isEqual(prevProps.policy, nextProps.policy) && - _.isEqual(prevParentReportAction, nextParentReportAction) + _.isEqual(prevParentReportAction, nextParentReportAction) && + _.isEqual(prevProps.reportActions, nextProps.reportActions) ); }), ); From 2a02be85220ff8dd7449bd9b42ebce20d9506390 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Thu, 7 Mar 2024 00:39:23 -0800 Subject: [PATCH 055/173] fix some malformed logic minor style and linting updates --- src/pages/home/ReportScreen.js | 13 ++++++------- src/pages/home/report/ReportActionItem.js | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 22858982544d..d0109bb8dc08 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -69,10 +69,7 @@ const propTypes = { allReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), /** All the report actions for this report */ - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), - - /** The report actions for the first transaction thread associated with the report */ - transactionThreadReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), /** The report's parentReportAction */ parentReportAction: PropTypes.shape(reportActionPropTypes), @@ -290,10 +287,12 @@ function ReportScreen({ const didSubscribeToReportLeavingEvents = useRef(false); const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]); const transactionThreadReportActions = useMemo(() => { - if (transactionThreadReportID) { - return null; + if (transactionThreadReportID === '0') { + return []; } - return ReportActionsUtils.getSortedReportActionsForDisplay(Object.values(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportReportID}`])); + + const reportActions = Object.values(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {}); + return ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); }, [allReportActions, transactionThreadReportID]); useEffect(() => { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9d0b555bba33..ee728117dcd6 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -168,10 +168,10 @@ function ReportActionItem(props) { const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID; const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(props.reportActions); const transactionThreadReport = useMemo(() => { - if (transactionThreadReportID) { - return null; + if (transactionThreadReportID === '0') { + return {}; } - return props.reports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportReportID}`]; + return props.reports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? {}; }, [props.reports, transactionThreadReportID]); const reportScrollManager = useReportScrollManager(); From b5ae144058eef38eccdedfc7f9858f336830b3d4 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Thu, 7 Mar 2024 01:06:01 -0800 Subject: [PATCH 056/173] ensure we don't show outdated UI due to removed IOU requests --- src/libs/ReportActionsUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 08a97728eefe..628d88fdc76f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -218,6 +218,7 @@ function getOneTransactionThreadReportID(reportActions: ReportActions): string { action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (iouRequestTypes.includes(action.originalMessage.type) ?? []) && action.childReportID + && action.originalMessage.IOUTransactionID ); // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report and we don't want to return the transactionThreadReportActions From 77a9b0dd8b7e9b81b11263ea430ccdcbe7ff3f1a Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 7 Mar 2024 14:47:59 +0100 Subject: [PATCH 057/173] start migrating CustomVersionFilePlugin to TypeScript --- ...ustomVersionFilePlugin.js => CustomVersionFilePlugin.ts} | 6 ++++-- config/webpack/webpack.dev.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) rename config/webpack/{CustomVersionFilePlugin.js => CustomVersionFilePlugin.ts} (91%) diff --git a/config/webpack/CustomVersionFilePlugin.js b/config/webpack/CustomVersionFilePlugin.ts similarity index 91% rename from config/webpack/CustomVersionFilePlugin.js rename to config/webpack/CustomVersionFilePlugin.ts index ed7c0f3dca95..4003429341c6 100644 --- a/config/webpack/CustomVersionFilePlugin.js +++ b/config/webpack/CustomVersionFilePlugin.ts @@ -1,3 +1,5 @@ +import type {Compiler} from 'webpack'; + const fs = require('fs'); const path = require('path'); const APP_VERSION = require('../../package.json').version; @@ -6,7 +8,7 @@ const APP_VERSION = require('../../package.json').version; * Simple webpack plugin that writes the app version (from package.json) and the webpack hash to './version.json' */ class CustomVersionFilePlugin { - apply(compiler) { + apply(compiler: Compiler) { compiler.hooks.done.tap( this.constructor.name, () => @@ -30,4 +32,4 @@ class CustomVersionFilePlugin { } } -module.exports = CustomVersionFilePlugin; +export default CustomVersionFilePlugin; diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 56dc735edd86..654ee20f2a6e 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type webpack from 'webpack'; +import getCommonConfig from './webpack.common'; import portfinder = require('portfinder'); import TimeAnalyticsWebpackPlugin = require('time-analytics-webpack-plugin'); -import getCommonConfig from './webpack.common'; const {TimeAnalyticsPlugin} = TimeAnalyticsWebpackPlugin; const path = require('path'); From f6cfcb399ee646c46a3b9ae82ba7b781c91bed33 Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Thu, 7 Mar 2024 13:19:08 -0800 Subject: [PATCH 058/173] use transaction currency instead of checking between transactionThreadReport and report --- src/pages/home/ReportScreen.js | 2 +- src/pages/home/report/ReportActionItem.js | 29 +++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d0109bb8dc08..d6cb010373f9 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -291,7 +291,7 @@ function ReportScreen({ return []; } - const reportActions = Object.values(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {}); + const reportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? []; return ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); }, [allReportActions, transactionThreadReportID]); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index ee728117dcd6..fb4a1f52b51a 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -62,7 +62,6 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; @@ -78,6 +77,7 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; +import transactionPropTypes from '@components/transactionPropTypes'; const propTypes = { ...windowDimensionsPropTypes, @@ -129,6 +129,9 @@ const propTypes = { /** All the report actions belonging to the current report */ reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** All the transactions shared wit hthe user */ + transactions: PropTypes.objectOf(PropTypes.shape(transactionPropTypes)), + /** Callback to be called on onPress */ onPress: PropTypes.func, }; @@ -144,6 +147,7 @@ const defaultProps = { userWallet: {}, parentReportActions: {}, reportActions: {}, + transactions: {}, onPress: undefined, }; @@ -167,13 +171,20 @@ function ReportActionItem(props) { const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID; const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(props.reportActions); + let transaction = {}; const transactionThreadReport = useMemo(() => { if (transactionThreadReportID === '0') { return {}; } - return props.reports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? {}; - }, [props.reports, transactionThreadReportID]); + const report = props.reports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? {}; + + // Get the transaction associated with the report + const transactionID = props.reportActions?.[report.parentReportActionID ?? '']?.originalMessage?.IOUTransactionID ?? 0; + transaction = props.transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + return report; + }, [props.reports, transactionThreadReportID, props.reportActions, props.transactions]); + const transactionCurrency = !_.isEmpty(transaction) ? (transaction.modifiedCurrency ?? transaction.currency) : props.report.currency; const reportScrollManager = useReportScrollManager(); const highlightedBackgroundColorIfNeeded = useMemo( @@ -707,9 +718,9 @@ function ReportActionItem(props) { if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { return ( - {transactionThreadReport && !isEmptyObject(transactionThreadReport) ? ( + {transactionThreadReport && !_.isEmpty(transactionThreadReport) ? ( <> - {transactionThreadReport.currency !== props.report.currency && ( + {transactionCurrency !== props.report.currency && ( @@ -921,6 +932,9 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID || 0}`, canEvict: false, }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, }), )( memo(ReportActionItem, (prevProps, nextProps) => { @@ -958,7 +972,8 @@ export default compose( _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) && _.isEqual(prevProps.policy, nextProps.policy) && _.isEqual(prevParentReportAction, nextParentReportAction) && - _.isEqual(prevProps.reportActions, nextProps.reportActions) + _.isEqual(prevProps.reportActions, nextProps.reportActions) && + _.isEqual(prevProps.transactions, nextProps.transactions) ); }), ); From 0140912da9f85a1eb17e7942a517c048c64b3c0f Mon Sep 17 00:00:00 2001 From: NikkiWines Date: Thu, 7 Mar 2024 22:58:34 -0800 Subject: [PATCH 059/173] merge main and address conflicts from ts migrations for ReportActionItem --- .eslintrc.js | 3 +- .../javascript/authorChecklist/index.js | 16 +- .../javascript/awaitStagingDeploys/index.js | 16 +- .../javascript/checkDeployBlockers/index.js | 16 +- .../createOrUpdateStagingDeploy.js | 11 +- .../createOrUpdateStagingDeploy/index.js | 27 +- .../javascript/getArtifactInfo/index.js | 16 +- .../getDeployPullRequestList/index.js | 16 +- .../javascript/getPullRequestDetails/index.js | 16 +- .../javascript/getReleaseBody/index.js | 16 +- .../javascript/isStagingDeployLocked/index.js | 16 +- .../markPullRequestsAsDeployed/index.js | 16 +- .../javascript/postTestBuildComment/index.js | 16 +- .../reopenIssueWithComment/index.js | 16 +- .../javascript/reviewerChecklist/index.js | 16 +- .../javascript/verifySignedCommits/index.js | 16 +- .github/libs/GithubUtils.js | 16 +- android/app/build.gradle | 4 +- .../Change-or-add-email-address.md | 24 + docs/redirects.csv | 9 +- ios/NewExpensify/Info.plist | 4 +- ios/NewExpensifyTests/Info.plist | 4 +- ios/NotificationServiceExtension/Info.plist | 4 +- package-lock.json | 4 +- package.json | 2 +- src/CONST.ts | 674 ++++++++++++++ src/ONYXKEYS.ts | 13 +- src/ROUTES.ts | 17 + src/SCREENS.ts | 4 + src/components/AddressSearch/index.tsx | 3 +- .../AttachmentCarousel/CarouselItem.js | 1 - .../Attachments/AttachmentView/index.js | 3 +- src/components/Badge.tsx | 34 +- src/components/Button/index.tsx | 2 +- src/components/DistanceEReceipt.tsx | 3 +- src/components/DistanceRequest/index.tsx | 1 + src/components/DraggableList/index.tsx | 6 +- src/components/FloatingActionButton.tsx | 11 +- src/components/Form/FormWrapper.tsx | 8 +- src/components/FormScrollView.tsx | 7 +- src/components/HeaderPageLayout.tsx | 3 +- src/components/KYCWall/BaseKYCWall.tsx | 12 +- src/components/MoneyReportHeader.tsx | 2 +- .../MoneyRequestConfirmationList.js | 8 +- src/components/MoneyRequestHeader.tsx | 16 +- ...oraryForRefactorRequestConfirmationList.js | 34 +- src/components/OnyxProvider.tsx | 6 +- .../OptionsSelector/BaseOptionsSelector.js | 3 +- src/components/PDFView/PDFPasswordForm.js | 3 +- src/components/Picker/BasePicker.tsx | 1 + src/components/Popover/types.ts | 5 +- src/components/PopoverProvider/index.tsx | 7 +- src/components/PopoverProvider/types.ts | 11 +- src/components/PopoverWithoutOverlay/types.ts | 5 +- .../Pressable/GenericPressable/types.ts | 5 +- .../types.ts | 2 +- .../ReportActionItemEmojiReactions.tsx | 4 +- .../ReportActionItem/MoneyReportView.tsx | 175 ++-- .../ReportActionItem/MoneyRequestView.tsx | 5 +- .../ReportActionItem/TaskAction.tsx | 10 +- src/components/ScrollView.tsx | 28 + src/components/ScrollViewWithContext.tsx | 11 +- src/components/VideoPlayer/BaseVideoPlayer.js | 8 +- src/components/createOnyxContext.tsx | 8 +- src/languages/en.ts | 13 + src/languages/es.ts | 13 + src/libs/API/parameters/AcceptJoinRequest.ts | 5 + .../CreateWorkspaceCategoriesParams.ts | 10 + src/libs/API/parameters/DeclineJoinRequest.ts | 5 + .../API/parameters/JoinPolicyInviteLink.ts | 6 + .../API/parameters/PayMoneyRequestParams.ts | 1 + src/libs/API/parameters/index.ts | 4 + src/libs/API/types.ts | 10 + src/libs/DistanceRequestUtils.ts | 29 +- src/libs/EmojiUtils.ts | 16 +- src/libs/ErrorUtils.ts | 2 + .../Navigation/AppNavigator/AuthScreens.tsx | 9 + .../AppNavigator/ModalStackNavigators.tsx | 3 + .../BottomTabBar.tsx | 4 +- src/libs/Navigation/NavigationRoot.tsx | 2 +- .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 4 +- src/libs/Navigation/linkingConfig/config.ts | 10 + src/libs/Navigation/types.ts | 18 + .../PushNotification/NotificationType.ts | 2 + ...bscribeToReportCommentPushNotifications.ts | 26 +- .../reportWithoutHasDraftSelector.ts | 5 +- src/libs/OptionsListUtils.ts | 21 +- src/libs/Permissions.ts | 5 + src/libs/PolicyUtils.ts | 14 +- src/libs/Pusher/pusher.ts | 8 +- src/libs/PusherUtils.ts | 8 +- src/libs/ReportActionsUtils.ts | 43 +- src/libs/ReportUtils.ts | 219 +++-- src/libs/SidebarUtils.ts | 8 +- src/libs/TaskUtils.ts | 18 +- src/libs/actions/IOU.ts | 60 +- src/libs/actions/OnyxUpdateManager.ts | 3 +- src/libs/actions/OnyxUpdates.ts | 17 +- src/libs/actions/Policy.ts | 214 ++++- src/libs/actions/Report.ts | 30 +- src/libs/actions/Task.ts | 24 +- src/libs/actions/User.ts | 26 +- src/libs/calculateAnchorPosition.ts | 6 +- .../index.android.ts | 2 +- .../focusTextInputAfterAnimation/index.ts | 2 +- .../focusTextInputAfterAnimation/types.ts | 2 +- src/libs/getClickedTargetLocation/types.ts | 2 +- src/libs/isReportMessageAttachment.ts | 10 +- .../KeyReportActionsDraftByReportActionID.ts | 1 + .../navigateAfterJoinRequest/index.desktop.ts | 8 + src/libs/navigateAfterJoinRequest/index.ts | 8 + .../navigateAfterJoinRequest/index.web.ts | 8 + src/pages/DetailsPage.tsx | 3 +- src/pages/EnablePayments/TermsStep.js | 2 +- src/pages/FlagCommentPage.tsx | 3 +- src/pages/GetAssistancePage.tsx | 3 +- src/pages/KeyboardShortcutsPage.tsx | 3 +- .../ManageTeamsExpensesPage.tsx | 3 +- .../PurposeForUsingExpensifyPage.tsx | 3 +- .../PrivateNotes/PrivateNotesListPage.tsx | 2 +- src/pages/ProfilePage.js | 3 +- .../ReimbursementAccount/BankAccountStep.js | 3 +- .../BankInfo/substeps/Confirmation.tsx | 3 +- .../ConfirmationUBO.tsx | 3 +- .../substeps/CompanyOwnersListUBO.tsx | 3 +- .../substeps/ConfirmationBusiness.tsx | 2 +- .../components/FinishChatCard.tsx | 2 +- .../ContinueBankAccountSetup.js | 2 +- .../EnableBankAccount/EnableBankAccount.tsx | 2 +- .../PersonalInfo/substeps/Confirmation.tsx | 3 +- .../RequestorOnfidoStep.js | 2 +- .../VerifyIdentity/VerifyIdentity.tsx | 3 +- src/pages/ReportDetailsPage.tsx | 3 +- src/pages/ShareCodePage.tsx | 3 +- src/pages/home/ReportScreenContext.ts | 5 +- .../BaseReportActionContextMenu.tsx | 4 +- .../report/ContextMenu/ContextMenuActions.tsx | 21 +- .../MiniReportActionContextMenu/types.ts | 2 +- .../PopoverReportActionContextMenu.tsx | 8 +- .../ContextMenu/ReportActionContextMenu.ts | 2 +- ...portActionItem.js => ReportActionItem.tsx} | 839 +++++++++--------- .../report/ReportActionItemBasicMessage.tsx | 2 +- .../home/report/ReportActionItemCreated.tsx | 2 +- .../home/report/ReportActionItemFragment.tsx | 1 + .../report/ReportActionItemMessageEdit.tsx | 5 +- .../report/ReportActionItemParentAction.tsx | 1 - .../home/report/ReportActionItemSingle.tsx | 29 +- .../home/report/ReportActionItemThread.tsx | 3 +- src/pages/home/report/ReportActionsView.js | 7 +- src/pages/home/sidebar/AllSettingsScreen.tsx | 2 +- src/pages/iou/MoneyRequestSelectorPage.js | 4 +- src/pages/iou/request/IOURequestStartPage.js | 12 +- ...yForRefactorRequestParticipantsSelector.js | 14 +- .../iou/request/step/IOURequestStepTag.js | 8 +- .../iou/steps/MoneyRequestAmountForm.tsx | 3 +- .../MoneyRequestParticipantsSelector.js | 11 +- src/pages/settings/AboutPage/AboutPage.tsx | 3 +- src/pages/settings/AppDownloadLinks.tsx | 2 +- .../ExitSurvey/ExitSurveyConfirmPage.tsx | 8 +- src/pages/settings/InitialSettingsPage.tsx | 19 +- .../settings/Preferences/PreferencesPage.js | 3 +- .../Contacts/ContactMethodDetailsPage.tsx | 3 +- .../Profile/Contacts/ContactMethodsPage.tsx | 3 +- src/pages/settings/Profile/ProfilePage.js | 3 +- .../settings/Report/ReportSettingsPage.tsx | 3 +- .../Security/SecuritySettingsPage.tsx | 3 +- .../TwoFactorAuth/Steps/CodesStep.tsx | 3 +- .../TwoFactorAuth/Steps/EnabledStep.tsx | 3 +- .../TwoFactorAuth/Steps/VerifyStep.tsx | 3 +- .../settings/Wallet/ExpensifyCardPage.tsx | 3 +- .../settings/Wallet/TransferBalancePage.tsx | 3 +- .../settings/Wallet/WalletPage/WalletPage.tsx | 7 +- src/pages/signin/SAMLSignInPage/index.tsx | 2 +- src/pages/signin/SignInPageLayout/index.tsx | 7 +- src/pages/tasks/NewTaskPage.js | 12 +- ...Modal.js => TaskAssigneeSelectorModal.tsx} | 134 +-- ...riptionPage.js => TaskDescriptionPage.tsx} | 91 +- ... => TaskShareDestinationSelectorModal.tsx} | 83 +- .../{TaskTitlePage.js => TaskTitlePage.tsx} | 100 +-- src/pages/workspace/WorkspaceInitialPage.tsx | 3 +- src/pages/workspace/WorkspaceJoinUserPage.tsx | 80 ++ src/pages/workspace/WorkspaceMembersPage.tsx | 60 +- src/pages/workspace/WorkspaceProfilePage.tsx | 39 +- .../workspace/WorkspaceProfileSharePage.tsx | 3 +- src/pages/workspace/WorkspacesListPage.tsx | 50 +- src/pages/workspace/WorkspacesListRow.tsx | 49 +- .../categories/CreateCategoryPage.tsx | 110 +++ .../categories/WorkspaceCategoriesPage.tsx | 69 +- .../members/WorkspaceMemberDetailsPage.tsx | 152 ++++ ...orkspaceMemberDetailsRoleSelectionPage.tsx | 87 ++ .../WorkspaceRateAndUnitPage/InitialPage.tsx | 12 +- src/styles/utils/index.ts | 9 +- src/styles/utils/spacing.ts | 4 + src/types/form/WorkspaceCategoryCreateForm.ts | 18 + src/types/form/index.ts | 1 + src/types/onyx/LastSelectedDistanceRates.ts | 3 + src/types/onyx/OnyxUpdatesFromServer.ts | 2 +- src/types/onyx/OriginalMessage.ts | 18 +- src/types/onyx/Policy.ts | 18 +- src/types/onyx/PolicyJoinMember.ts | 17 + src/types/onyx/Task.ts | 2 +- src/types/onyx/index.ts | 10 +- src/utils/arrayDifference.ts | 9 + tests/e2e/utils/{logger.js => logger.ts} | 32 +- tests/perf-test/SidebarUtils.perf-test.ts | 129 +-- tests/ui/UnreadIndicatorsTest.js | 88 +- tests/unit/GithubUtilsTest.ts | 62 +- .../{LocalizeTests.js => LocalizeTests.ts} | 0 tests/unit/ReportActionsUtilsTest.ts | 7 +- tests/unit/ReportUtilsTest.js | 52 +- .../{TranslateTest.js => TranslateTest.ts} | 69 +- ....js => createOrUpdateStagingDeployTest.ts} | 15 +- ...metersTest.js => enhanceParametersTest.ts} | 1 + ...terTest.js => nativeVersionUpdaterTest.ts} | 0 tests/utils/PusherHelper.ts | 17 +- tests/utils/collections/reportActions.ts | 2 + 216 files changed, 3747 insertions(+), 1540 deletions(-) create mode 100644 docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md create mode 100644 src/components/ScrollView.tsx create mode 100644 src/libs/API/parameters/AcceptJoinRequest.ts create mode 100644 src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts create mode 100644 src/libs/API/parameters/DeclineJoinRequest.ts create mode 100644 src/libs/API/parameters/JoinPolicyInviteLink.ts create mode 100644 src/libs/navigateAfterJoinRequest/index.desktop.ts create mode 100644 src/libs/navigateAfterJoinRequest/index.ts create mode 100644 src/libs/navigateAfterJoinRequest/index.web.ts rename src/pages/home/report/{ReportActionItem.js => ReportActionItem.tsx} (51%) rename src/pages/tasks/{TaskAssigneeSelectorModal.js => TaskAssigneeSelectorModal.tsx} (63%) rename src/pages/tasks/{TaskDescriptionPage.js => TaskDescriptionPage.tsx} (61%) rename src/pages/tasks/{TaskShareDestinationSelectorModal.js => TaskShareDestinationSelectorModal.tsx} (62%) rename src/pages/tasks/{TaskTitlePage.js => TaskTitlePage.tsx} (50%) create mode 100644 src/pages/workspace/WorkspaceJoinUserPage.tsx create mode 100644 src/pages/workspace/categories/CreateCategoryPage.tsx create mode 100644 src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx create mode 100644 src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage.tsx create mode 100644 src/types/form/WorkspaceCategoryCreateForm.ts create mode 100644 src/types/onyx/LastSelectedDistanceRates.ts create mode 100644 src/types/onyx/PolicyJoinMember.ts create mode 100644 src/utils/arrayDifference.ts rename tests/e2e/utils/{logger.js => logger.ts} (66%) rename tests/unit/{LocalizeTests.js => LocalizeTests.ts} (100%) rename tests/unit/{TranslateTest.js => TranslateTest.ts} (63%) rename tests/unit/{createOrUpdateStagingDeployTest.js => createOrUpdateStagingDeployTest.ts} (97%) rename tests/unit/{enhanceParametersTest.js => enhanceParametersTest.ts} (96%) rename tests/unit/{nativeVersionUpdaterTest.js => nativeVersionUpdaterTest.ts} (100%) diff --git a/.eslintrc.js b/.eslintrc.js index c0c95d3f5686..5451cfff6534 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,13 +1,14 @@ const restrictedImportPaths = [ { name: 'react-native', - importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text'], + importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text', 'ScrollView'], message: [ '', "For 'useWindowDimensions', please use 'src/hooks/useWindowDimensions' instead.", "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", "For 'StatusBar', please use 'src/libs/StatusBar' instead.", "For 'Text', please use '@components/Text' instead.", + "For 'ScrollView', please use '@components/ScrollView' instead.", ].join('\n'), }, { diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 528a0a11498a..e267769dc457 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,7 +359,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index f042dbb38a91..dd2aef38e1ee 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -395,14 +395,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -437,11 +437,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -471,7 +471,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index 8e10f8b1d8b6..82092be7e0eb 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -362,14 +362,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -404,11 +404,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -438,7 +438,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js index 4441348a3c36..1752ae62f86c 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js @@ -40,8 +40,11 @@ async function run() { // Next, we generate the checklist body let checklistBody = ''; + let checklistAssignees = []; if (shouldCreateNewDeployChecklist) { - checklistBody = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); + const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); + checklistBody = issueBody; + checklistAssignees = issueAssignees; } else { // Generate the updated PR list, preserving the previous state of `isVerified` for existing PRs const PRList = _.reduce( @@ -94,7 +97,7 @@ async function run() { } const didVersionChange = newVersionTag !== currentChecklistData.tag; - checklistBody = await GithubUtils.generateStagingDeployCashBody( + const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody( newVersionTag, _.pluck(PRList, 'url'), _.pluck(_.where(PRList, {isVerified: true}), 'url'), @@ -105,6 +108,8 @@ async function run() { didVersionChange ? false : currentChecklistData.isFirebaseChecked, didVersionChange ? false : currentChecklistData.isGHStatusChecked, ); + checklistBody = issueBody; + checklistAssignees = issueAssignees; } // Finally, create or update the checklist @@ -119,7 +124,7 @@ async function run() { ...defaultPayload, title: `Deploy Checklist: New Expensify ${format(new Date(), CONST.DATE_FORMAT_STRING)}`, labels: [CONST.LABELS.STAGING_DEPLOY], - assignees: [CONST.APPLAUSE_BOT], + assignees: [CONST.APPLAUSE_BOT].concat(checklistAssignees), }); console.log(`Successfully created new StagingDeployCash! 🎉 ${newChecklist.html_url}`); return newChecklist; diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 154dacbdc3c3..9c9a42709af0 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -49,8 +49,11 @@ async function run() { // Next, we generate the checklist body let checklistBody = ''; + let checklistAssignees = []; if (shouldCreateNewDeployChecklist) { - checklistBody = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); + const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody(newVersionTag, _.map(mergedPRs, GithubUtils.getPullRequestURLFromNumber)); + checklistBody = issueBody; + checklistAssignees = issueAssignees; } else { // Generate the updated PR list, preserving the previous state of `isVerified` for existing PRs const PRList = _.reduce( @@ -103,7 +106,7 @@ async function run() { } const didVersionChange = newVersionTag !== currentChecklistData.tag; - checklistBody = await GithubUtils.generateStagingDeployCashBody( + const {issueBody, issueAssignees} = await GithubUtils.generateStagingDeployCashBody( newVersionTag, _.pluck(PRList, 'url'), _.pluck(_.where(PRList, {isVerified: true}), 'url'), @@ -114,6 +117,8 @@ async function run() { didVersionChange ? false : currentChecklistData.isFirebaseChecked, didVersionChange ? false : currentChecklistData.isGHStatusChecked, ); + checklistBody = issueBody; + checklistAssignees = issueAssignees; } // Finally, create or update the checklist @@ -128,7 +133,7 @@ async function run() { ...defaultPayload, title: `Deploy Checklist: New Expensify ${format(new Date(), CONST.DATE_FORMAT_STRING)}`, labels: [CONST.LABELS.STAGING_DEPLOY], - assignees: [CONST.APPLAUSE_BOT], + assignees: [CONST.APPLAUSE_BOT].concat(checklistAssignees), }); console.log(`Successfully created new StagingDeployCash! 🎉 ${newChecklist.html_url}`); return newChecklist; @@ -434,14 +439,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -476,11 +481,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -510,7 +515,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index ea56ff5f4ebd..e4f7634bd849 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -321,14 +321,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -363,11 +363,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -397,7 +397,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index f272929d536a..f941c9524856 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -377,14 +377,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -419,11 +419,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -453,7 +453,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index b8d7d821d64e..f4168af28802 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -329,14 +329,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -371,11 +371,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -405,7 +405,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/getReleaseBody/index.js b/.github/actions/javascript/getReleaseBody/index.js index cc1321ce5cd5..547aafe23038 100644 --- a/.github/actions/javascript/getReleaseBody/index.js +++ b/.github/actions/javascript/getReleaseBody/index.js @@ -329,14 +329,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -371,11 +371,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -405,7 +405,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index 8124c5795a5a..4938b5bb7745 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -313,14 +313,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -355,11 +355,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -389,7 +389,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 36cd0aaefe4a..2e6ab7e018dd 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -478,14 +478,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -520,11 +520,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -554,7 +554,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 329e0d3aad5d..9dd23d68ca0a 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -388,14 +388,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -430,11 +430,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -464,7 +464,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 6a5f89badb5e..42196053f63f 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,7 +359,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 322b529b89bf..22335b36bd2b 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,7 +359,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index ba188d3a2b86..239f20c9d258 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -283,14 +283,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -325,11 +325,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -359,7 +359,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index 0cd407c78153..e988167850ec 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -250,14 +250,14 @@ class GithubUtils { .then((data) => { // The format of this map is following: // { - // 'https://github.com/Expensify/App/pull/9641': [ 'PauloGasparSv', 'kidroca' ], - // 'https://github.com/Expensify/App/pull/9642': [ 'mountiny', 'kidroca' ] + // 'https://github.com/Expensify/App/pull/9641': 'PauloGasparSv', + // 'https://github.com/Expensify/App/pull/9642': 'mountiny' // } const internalQAPRMap = _.reduce( _.filter(data, (pr) => !_.isEmpty(_.findWhere(pr.labels, {name: CONST.LABELS.INTERNAL_QA}))), (map, pr) => { // eslint-disable-next-line no-param-reassign - map[pr.html_url] = _.compact(_.pluck(pr.assignees, 'login')); + map[pr.html_url] = pr.merged_by.login; return map; }, {}, @@ -292,11 +292,11 @@ class GithubUtils { if (!_.isEmpty(internalQAPRMap)) { console.log('Found the following verified Internal QA PRs:', resolvedInternalQAPRs); issueBody += '**Internal QA:**\r\n'; - _.each(internalQAPRMap, (assignees, URL) => { - const assigneeMentions = _.reduce(assignees, (memo, assignee) => `${memo} @${assignee}`, ''); + _.each(internalQAPRMap, (merger, URL) => { + const mergerMention = `@${merger}`; issueBody += `${_.contains(resolvedInternalQAPRs, URL) ? '- [x]' : '- [ ]'} `; issueBody += `${URL}`; - issueBody += ` -${assigneeMentions}`; + issueBody += ` - ${mergerMention}`; issueBody += '\r\n'; }); issueBody += '\r\n\r\n'; @@ -326,7 +326,9 @@ class GithubUtils { issueBody += `\r\n- [${isGHStatusChecked ? 'x' : ' '}] I checked [GitHub Status](https://www.githubstatus.com/) and verified there is no reported incident with Actions.`; issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; - return issueBody; + const issueAssignees = _.values(internalQAPRMap); + const issue = {issueBody, issueAssignees}; + return issue; }) .catch((err) => console.warn('Error generating StagingDeployCash issue body! Continuing...', err)); } diff --git a/android/app/build.gradle b/android/app/build.gradle index aedb0b9fbc13..cbe11156b093 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044707 - versionName "1.4.47-7" + versionCode 1001044900 + versionName "1.4.49-0" } flavorDimensions "default" diff --git a/docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md b/docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md new file mode 100644 index 000000000000..754b9a7f9ac0 --- /dev/null +++ b/docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md @@ -0,0 +1,24 @@ +--- +title: Change or add email address +description: Update your Expensify email address or add a secondary email +--- +
+ +The primary email address on your Expensify account is the email that receives email updates and notifications for your account. You can add a secondary email address in order to +- Change your primary email to a new one. +- Connect your personal email address as a secondary login if your primary email address is one from your employer. This allows you to always have access to your Expensify account, even if your employer changes. + +{% include info.html %} +Before you can remove a primary email address, you must add a new one to your Expensify account and make it the primary using the steps below. Email addresses must be added as a secondary login before they can be made the primary. +{% include end-info.html %} + +*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* + +1. Hover over Settings, then click **Account**. +2. Under the Account Details tab, scroll down to the Secondary Logins section and click **Add Secondary Login**. +3. Enter the email address or phone number you wish to use as a secondary login. For phone numbers, be sure to include the international code, if applicable. +4. Find the email or text message from Expensify containing the Magic Code and enter it into the field. +5. To make the new email address the primary address for your account, click **Make Primary**. + +You can keep both logins, or you can click **Remove** next to the old email address to delete it from your account. +
diff --git a/docs/redirects.csv b/docs/redirects.csv index 4ed309467f13..097c0ad2679e 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -46,8 +46,8 @@ https://community.expensify.com/discussion/5366/deep-dive-troubleshooting-credit https://community.expensify.com/discussion/9554/how-to-set-up-global-reimbursemen,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements https://community.expensify.com/discussion/4463/how-to-remove-or-manage-settings-for-imported-personal-cards,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards -https://community.expensify.com/discussion/4826/how-to-set-your-annual-subscription-size,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription -https://community.expensify.com/discussion/5667/deep-dive-how-does-the-annual-subscription-billing-work,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription +https://community.expensify.com/discussion/4826/how-to-set-your-annual-subscription-size,https://use.expensify.com/ +https://community.expensify.com/discussion/5667/deep-dive-how-does-the-annual-subscription-billing-work,https://use.expensify.com/ https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager https://help.expensify.com/expensify-classic/hubs/getting-started/plan-types,https://use.expensify.com/ https://help.expensify.com/articles/expensify-classic/getting-started/Employees,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace @@ -60,3 +60,8 @@ https://help.expensify.com/articles/expensify-classic/account-settings/Preferenc https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://help.expensify.com/articles/expensify-classic/getting-started/Individual-Users,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members +https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts +https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5d93786a5ad6..bccea916e01a 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.47 + 1.4.49 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.47.7 + 1.4.49.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index fad0a170d4ab..058476f03a9d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.47 + 1.4.49 CFBundleSignature ???? CFBundleVersion - 1.4.47.7 + 1.4.49.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 220fdd322c6e..869b3aebab44 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.47 + 1.4.49 CFBundleVersion - 1.4.47.7 + 1.4.49.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index cc717e8d6a0f..bfab80fe3148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.47-7", + "version": "1.4.49-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.47-7", + "version": "1.4.49-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5b498cb09dc2..a78c23c3c960 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.47-7", + "version": "1.4.49-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index 1e3b33d5d760..70fecab70c39 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6,6 +6,12 @@ import * as KeyCommand from 'react-native-key-command'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; +type RateAndUnit = { + unit: string; + rate: number; +}; +type CurrencyDefaultMileageRate = Record; + // Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types. // Freezing the array ensures that it cannot be unintentionally modified. const EMPTY_ARRAY = Object.freeze([]); @@ -313,6 +319,7 @@ const CONST = { BETA_COMMENT_LINKING: 'commentLinking', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', + P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', }, BUTTON_STATES: { @@ -568,6 +575,7 @@ const CONST = { LIMIT: 50, TYPE: { ADDCOMMENT: 'ADDCOMMENT', + ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST', APPROVED: 'APPROVED', CHRONOSOOOLIST: 'CHRONOSOOOLIST', CLOSED: 'CLOSED', @@ -671,6 +679,10 @@ const CONST = { INVITE: 'invited', NOTHING: 'nothing', }, + ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION: { + ACCEPT: 'accept', + DECLINE: 'decline', + }, ARCHIVE_REASON: { DEFAULT: 'default', ACCOUNT_CLOSED: 'accountClosed', @@ -1414,6 +1426,7 @@ const CONST = { MILEAGE_IRS_RATE: 0.655, DEFAULT_RATE: 'Default Rate', RATE_DECIMALS: 3, + FAKE_P2P_ID: '_FAKE_P2P_ID_', }, TERMS: { @@ -1646,6 +1659,7 @@ const CONST = { FORM_CHARACTER_LIMIT: 50, LEGAL_NAMES_CHARACTER_LIMIT: 150, LOGIN_CHARACTER_LIMIT: 254, + CATEGORY_NAME_LIMIT: 256, TITLE_CHARACTER_LIMIT: 100, DESCRIPTION_LIMIT: 500, @@ -1726,6 +1740,7 @@ const CONST = { MAX_64BIT_LEFT_PART: 92233, MAX_64BIT_MIDDLE_PART: 7203685, MAX_64BIT_RIGHT_PART: 4775807, + INVALID_CATEGORY_NAME: '###', // When generating a random value to fit in 7 digits (for the `middle` or `right` parts above), this is the maximum value to multiply by Math.random(). MAX_INT_FOR_RANDOM_7_DIGIT_VALUE: 10000000, @@ -3108,6 +3123,7 @@ const CONST = { ONYX_UPDATE_TYPES: { HTTPS: 'https', PUSHER: 'pusher', + AIRSHIP: 'airship', }, EVENTS: { SCROLLING: 'scrolling', @@ -3340,6 +3356,664 @@ const CONST = { ADDRESS: 3, }, }, + CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{ + "AED": { + "rate": 396, + "unit": "km" + }, + "AFN": { + "rate": 8369, + "unit": "km" + }, + "ALL": { + "rate": 11104, + "unit": "km" + }, + "AMD": { + "rate": 56842, + "unit": "km" + }, + "ANG": { + "rate": 193, + "unit": "km" + }, + "AOA": { + "rate": 67518, + "unit": "km" + }, + "ARS": { + "rate": 9873, + "unit": "km" + }, + "AUD": { + "rate": 85, + "unit": "km" + }, + "AWG": { + "rate": 195, + "unit": "km" + }, + "AZN": { + "rate": 183, + "unit": "km" + }, + "BAM": { + "rate": 177, + "unit": "km" + }, + "BBD": { + "rate": 216, + "unit": "km" + }, + "BDT": { + "rate": 9130, + "unit": "km" + }, + "BGN": { + "rate": 177, + "unit": "km" + }, + "BHD": { + "rate": 40, + "unit": "km" + }, + "BIF": { + "rate": 210824, + "unit": "km" + }, + "BMD": { + "rate": 108, + "unit": "km" + }, + "BND": { + "rate": 145, + "unit": "km" + }, + "BOB": { + "rate": 745, + "unit": "km" + }, + "BRL": { + "rate": 594, + "unit": "km" + }, + "BSD": { + "rate": 108, + "unit": "km" + }, + "BTN": { + "rate": 7796, + "unit": "km" + }, + "BWP": { + "rate": 1180, + "unit": "km" + }, + "BYN": { + "rate": 280, + "unit": "km" + }, + "BYR": { + "rate": 2159418, + "unit": "km" + }, + "BZD": { + "rate": 217, + "unit": "km" + }, + "CAD": { + "rate": 70, + "unit": "km" + }, + "CDF": { + "rate": 213674, + "unit": "km" + }, + "CHF": { + "rate": 100, + "unit": "km" + }, + "CLP": { + "rate": 77249, + "unit": "km" + }, + "CNY": { + "rate": 702, + "unit": "km" + }, + "COP": { + "rate": 383668, + "unit": "km" + }, + "CRC": { + "rate": 65899, + "unit": "km" + }, + "CUC": { + "rate": 108, + "unit": "km" + }, + "CUP": { + "rate": 2776, + "unit": "km" + }, + "CVE": { + "rate": 6112, + "unit": "km" + }, + "CZK": { + "rate": 2356, + "unit": "km" + }, + "DJF": { + "rate": 19151, + "unit": "km" + }, + "DKK": { + "rate": 673, + "unit": "km" + }, + "DOP": { + "rate": 6144, + "unit": "km" + }, + "DZD": { + "rate": 14375, + "unit": "km" + }, + "EEK": { + "rate": 1576, + "unit": "km" + }, + "EGP": { + "rate": 1696, + "unit": "km" + }, + "ERN": { + "rate": 1617, + "unit": "km" + }, + "ETB": { + "rate": 4382, + "unit": "km" + }, + "EUR": { + "rate": 3, + "unit": "km" + }, + "FJD": { + "rate": 220, + "unit": "km" + }, + "FKP": { + "rate": 77, + "unit": "km" + }, + "GBP": { + "rate": 45, + "unit": "mi" + }, + "GEL": { + "rate": 359, + "unit": "km" + }, + "GHS": { + "rate": 620, + "unit": "km" + }, + "GIP": { + "rate": 77, + "unit": "km" + }, + "GMD": { + "rate": 5526, + "unit": "km" + }, + "GNF": { + "rate": 1081319, + "unit": "km" + }, + "GTQ": { + "rate": 832, + "unit": "km" + }, + "GYD": { + "rate": 22537, + "unit": "km" + }, + "HKD": { + "rate": 837, + "unit": "km" + }, + "HNL": { + "rate": 2606, + "unit": "km" + }, + "HRK": { + "rate": 684, + "unit": "km" + }, + "HTG": { + "rate": 8563, + "unit": "km" + }, + "HUF": { + "rate": 33091, + "unit": "km" + }, + "IDR": { + "rate": 1555279, + "unit": "km" + }, + "ILS": { + "rate": 356, + "unit": "km" + }, + "INR": { + "rate": 7805, + "unit": "km" + }, + "IQD": { + "rate": 157394, + "unit": "km" + }, + "IRR": { + "rate": 4539961, + "unit": "km" + }, + "ISK": { + "rate": 13518, + "unit": "km" + }, + "JMD": { + "rate": 15794, + "unit": "km" + }, + "JOD": { + "rate": 77, + "unit": "km" + }, + "JPY": { + "rate": 11748, + "unit": "km" + }, + "KES": { + "rate": 11845, + "unit": "km" + }, + "KGS": { + "rate": 9144, + "unit": "km" + }, + "KHR": { + "rate": 437658, + "unit": "km" + }, + "KMF": { + "rate": 44418, + "unit": "km" + }, + "KPW": { + "rate": 97043, + "unit": "km" + }, + "KRW": { + "rate": 121345, + "unit": "km" + }, + "KWD": { + "rate": 32, + "unit": "km" + }, + "KYD": { + "rate": 90, + "unit": "km" + }, + "KZT": { + "rate": 45396, + "unit": "km" + }, + "LAK": { + "rate": 1010829, + "unit": "km" + }, + "LBP": { + "rate": 164153, + "unit": "km" + }, + "LKR": { + "rate": 21377, + "unit": "km" + }, + "LRD": { + "rate": 18709, + "unit": "km" + }, + "LSL": { + "rate": 1587, + "unit": "km" + }, + "LTL": { + "rate": 348, + "unit": "km" + }, + "LVL": { + "rate": 71, + "unit": "km" + }, + "LYD": { + "rate": 486, + "unit": "km" + }, + "MAD": { + "rate": 967, + "unit": "km" + }, + "MDL": { + "rate": 1910, + "unit": "km" + }, + "MGA": { + "rate": 406520, + "unit": "km" + }, + "MKD": { + "rate": 5570, + "unit": "km" + }, + "MMK": { + "rate": 152083, + "unit": "km" + }, + "MNT": { + "rate": 306788, + "unit": "km" + }, + "MOP": { + "rate": 863, + "unit": "km" + }, + "MRO": { + "rate": 38463, + "unit": "km" + }, + "MRU": { + "rate": 3862, + "unit": "km" + }, + "MUR": { + "rate": 4340, + "unit": "km" + }, + "MVR": { + "rate": 1667, + "unit": "km" + }, + "MWK": { + "rate": 84643, + "unit": "km" + }, + "MXN": { + "rate": 2219, + "unit": "km" + }, + "MYR": { + "rate": 444, + "unit": "km" + }, + "MZN": { + "rate": 7772, + "unit": "km" + }, + "NAD": { + "rate": 1587, + "unit": "km" + }, + "NGN": { + "rate": 42688, + "unit": "km" + }, + "NIO": { + "rate": 3772, + "unit": "km" + }, + "NOK": { + "rate": 917, + "unit": "km" + }, + "NPR": { + "rate": 12474, + "unit": "km" + }, + "NZD": { + "rate": 151, + "unit": "km" + }, + "OMR": { + "rate": 42, + "unit": "km" + }, + "PAB": { + "rate": 108, + "unit": "km" + }, + "PEN": { + "rate": 401, + "unit": "km" + }, + "PGK": { + "rate": 380, + "unit": "km" + }, + "PHP": { + "rate": 5234, + "unit": "km" + }, + "PKR": { + "rate": 16785, + "unit": "km" + }, + "PLN": { + "rate": 415, + "unit": "km" + }, + "PYG": { + "rate": 704732, + "unit": "km" + }, + "QAR": { + "rate": 393, + "unit": "km" + }, + "RON": { + "rate": 443, + "unit": "km" + }, + "RSD": { + "rate": 10630, + "unit": "km" + }, + "RUB": { + "rate": 8074, + "unit": "km" + }, + "RWF": { + "rate": 107182, + "unit": "km" + }, + "SAR": { + "rate": 404, + "unit": "km" + }, + "SBD": { + "rate": 859, + "unit": "km" + }, + "SCR": { + "rate": 2287, + "unit": "km" + }, + "SDG": { + "rate": 41029, + "unit": "km" + }, + "SEK": { + "rate": 917, + "unit": "km" + }, + "SGD": { + "rate": 145, + "unit": "km" + }, + "SHP": { + "rate": 77, + "unit": "km" + }, + "SLL": { + "rate": 1102723, + "unit": "km" + }, + "SOS": { + "rate": 62604, + "unit": "km" + }, + "SRD": { + "rate": 1526, + "unit": "km" + }, + "STD": { + "rate": 2223309, + "unit": "km" + }, + "STN": { + "rate": 2232, + "unit": "km" + }, + "SVC": { + "rate": 943, + "unit": "km" + }, + "SYP": { + "rate": 82077, + "unit": "km" + }, + "SZL": { + "rate": 1585, + "unit": "km" + }, + "THB": { + "rate": 3328, + "unit": "km" + }, + "TJS": { + "rate": 1230, + "unit": "km" + }, + "TMT": { + "rate": 378, + "unit": "km" + }, + "TND": { + "rate": 295, + "unit": "km" + }, + "TOP": { + "rate": 245, + "unit": "km" + }, + "TRY": { + "rate": 845, + "unit": "km" + }, + "TTD": { + "rate": 732, + "unit": "km" + }, + "TWD": { + "rate": 3055, + "unit": "km" + }, + "TZS": { + "rate": 250116, + "unit": "km" + }, + "UAH": { + "rate": 2985, + "unit": "km" + }, + "UGX": { + "rate": 395255, + "unit": "km" + }, + "USD": { + "rate": 67, + "unit": "mi" + }, + "UYU": { + "rate": 4777, + "unit": "km" + }, + "UZS": { + "rate": 1131331, + "unit": "km" + }, + "VEB": { + "rate": 679346, + "unit": "km" + }, + "VEF": { + "rate": 26793449, + "unit": "km" + }, + "VES": { + "rate": 194381905, + "unit": "km" + }, + "VND": { + "rate": 2487242, + "unit": "km" + }, + "VUV": { + "rate": 11748, + "unit": "km" + }, + "WST": { + "rate": 272, + "unit": "km" + }, + "XAF": { + "rate": 59224, + "unit": "km" + }, + "XCD": { + "rate": 291, + "unit": "km" + }, + "XOF": { + "rate": 59224, + "unit": "km" + }, + "XPF": { + "rate": 10783, + "unit": "km" + }, + "YER": { + "rate": 27037, + "unit": "km" + }, + "ZAR": { + "rate": 1588, + "unit": "km" + }, + "ZMK": { + "rate": 566489, + "unit": "km" + }, + "ZMW": { + "rate": 2377, + "unit": "km" + } + }`) as CurrencyDefaultMileageRate, EXIT_SURVEY: { REASONS: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f6b5c635e4ae..31e22491e2b9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as FormTypes from './types/form'; @@ -128,6 +128,9 @@ const ONYXKEYS = { /** This NVP contains the choice that the user made on the engagement modal */ NVP_INTRO_SELECTED: 'introSelected', + /** The NVP with the last distance rate used per policy */ + NVP_LAST_SELECTED_DISTANCE_RATES: 'lastSelectedDistanceRates', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -284,6 +287,7 @@ const ONYXKEYS = { POLICY_MEMBERS: 'policyMembers_', POLICY_DRAFTS: 'policyDrafts_', POLICY_MEMBERS_DRAFTS: 'policyMembersDrafts_', + POLICY_JOIN_MEMBER: 'policyJoinMember_', POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', @@ -326,6 +330,8 @@ const ONYXKEYS = { ADD_DEBIT_CARD_FORM: 'addDebitCardForm', ADD_DEBIT_CARD_FORM_DRAFT: 'addDebitCardFormDraft', WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', + WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate', + WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft', WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm', WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft', @@ -407,6 +413,7 @@ type AllOnyxKeys = DeepValueOf; type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; + [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; @@ -481,6 +488,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; + [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; }; type OnyxValuesMapping = { @@ -524,6 +532,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; + [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; [ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData; [ONYXKEYS.IS_PLAID_DISABLED]: boolean; @@ -582,7 +591,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping; type OnyxValueKey = keyof OnyxValuesMapping; type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey; -type OnyxValue = OnyxEntry; +type OnyxValue = TOnyxKey extends keyof OnyxCollectionValuesMapping ? OnyxCollection : OnyxEntry; type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`; /** If this type errors, it means that the `OnyxKey` type is missing some keys. */ diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cfc287ba2cdc..2ed9fbc3666e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -478,6 +478,10 @@ const ROUTES = { route: 'workspace/:policyID/avatar', getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const, }, + WORKSPACE_JOIN_USER: { + route: 'workspace/:policyID/join', + getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const, + }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, @@ -546,10 +550,23 @@ const ROUTES = { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, }, + WORKSPACE_CATEGORY_CREATE: { + route: 'workspace/:policyID/categories/new', + getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const, + }, WORKSPACE_TAGS: { route: 'workspace/:policyID/tags', getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, }, + WORKSPACE_MEMBER_DETAILS: { + route: 'workspace/:policyID/members/:accountID', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo), + }, + WORKSPACE_MEMBER_ROLE_SELECTION: { + route: 'workspace/:policyID/members/:accountID/role-selection', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo), + }, + // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2369fe435feb..6fc61aec61a0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -128,6 +128,7 @@ const SCREENS = { SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', SAML_SIGN_IN: 'SAMLSignIn', + WORKSPACE_JOIN_USER: 'WorkspaceJoinUser', MONEY_REQUEST: { MANUAL_TAB: 'manual', @@ -223,8 +224,11 @@ const SCREENS = { DESCRIPTION: 'Workspace_Profile_Description', SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', + CATEGORY_CREATE: 'Category_Create', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', + MEMBER_DETAILS: 'Workspace_Member_Details', + MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection', }, EDIT_REQUEST: { diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 39c91c2a0789..a2e3f5d9948e 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -1,11 +1,12 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; -import {ActivityIndicator, Keyboard, LogBox, ScrollView, View} from 'react-native'; +import {ActivityIndicator, Keyboard, LogBox, View} from 'react-native'; import type {LayoutChangeEvent} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; import type {GooglePlaceData, GooglePlaceDetail} from 'react-native-google-places-autocomplete'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import LocationErrorMessage from '@components/LocationErrorMessage'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index e924cb8c13e9..b2c9fed64467 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -109,7 +109,6 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} - isUsedInCarousel />
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index f6a56dc73088..461548f0d2b1 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; import React, {memo, useEffect, useState} from 'react'; -import {ActivityIndicator, ScrollView, View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; @@ -9,6 +9,7 @@ import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 5be33e6ff2ec..635645b0035b 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -2,8 +2,12 @@ import React, {useCallback} from 'react'; import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; +import Icon from './Icon'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Text from './Text'; @@ -31,11 +35,29 @@ type BadgeProps = { /** Callback to be called on onPress */ onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void; + + /** The icon asset to display to the left of the text */ + icon?: IconAsset | null; + + /** Any additional styles to pass to the left icon container. */ + iconStyles?: StyleProp; }; -function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) { +function Badge({ + success = false, + error = false, + pressable = false, + text, + environment = CONST.ENVIRONMENT.DEV, + badgeStyles, + textStyles, + onPress = () => {}, + icon, + iconStyles = [], +}: BadgeProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const theme = useTheme(); const textColorStyles = success || error ? styles.textWhite : undefined; const Wrapper = pressable ? PressableWithoutFeedback : View; @@ -53,6 +75,16 @@ function Badge({success = false, error = false, pressable = false, text, environ aria-label={!pressable ? text : undefined} accessible={false} > + {icon && ( + + + + )} , + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index 941d63c1bf94..fda0c5441734 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import EReceiptBackground from '@assets/images/eReceipt_background.svg'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +15,7 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import ImageSVG from './ImageSVG'; import PendingMapView from './MapView/PendingMapView'; +import ScrollView from './ScrollView'; import Text from './Text'; import ThumbnailImage from './ThumbnailImage'; diff --git a/src/components/DistanceRequest/index.tsx b/src/components/DistanceRequest/index.tsx index 9900656057ce..8920c9a4a92b 100644 --- a/src/components/DistanceRequest/index.tsx +++ b/src/components/DistanceRequest/index.tsx @@ -2,6 +2,7 @@ import type {RouteProp} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports import type {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; diff --git a/src/components/DraggableList/index.tsx b/src/components/DraggableList/index.tsx index dc78a3ce6222..418f3e93eac4 100644 --- a/src/components/DraggableList/index.tsx +++ b/src/components/DraggableList/index.tsx @@ -1,7 +1,9 @@ import React, {useCallback} from 'react'; import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd'; import type {OnDragEndResponder} from 'react-beautiful-dnd'; -import {ScrollView} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {ScrollView as RNScrollView} from 'react-native'; +import ScrollView from '@components/ScrollView'; import useThemeStyles from '@hooks/useThemeStyles'; import type {DraggableListProps} from './types'; import useDraggableInPortal from './useDraggableInPortal'; @@ -37,7 +39,7 @@ function DraggableList( // eslint-disable-next-line @typescript-eslint/naming-convention ListFooterComponent, }: DraggableListProps, - ref: React.ForwardedRef, + ref: React.ForwardedRef, ) { const styles = useThemeStyles(); /** diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 9b68916c4003..88938f31cd79 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -1,6 +1,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useRef} from 'react'; -import type {GestureResponderEvent, Role} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, Role, Text} from 'react-native'; import {Platform, View} from 'react-native'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; @@ -58,12 +59,12 @@ type FloatingActionButtonProps = { role: Role; }; -function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) { +function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) { const {success, buttonDefaultBG, textLight, textDark} = useTheme(); const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; const {translate} = useLocalize(); - const fabPressable = useRef(null); + const fabPressable = useRef(null); const sharedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; @@ -112,9 +113,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo { - fabPressable.current = el; + fabPressable.current = el ?? null; if (buttonRef && 'current' in buttonRef) { - buttonRef.current = el; + buttonRef.current = el ?? null; } }} accessibilityLabel={accessibilityLabel} diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 5615f3b87cfa..5c2488ca144a 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,13 +1,15 @@ import React, {useCallback, useMemo, useRef} from 'react'; import type {RefObject} from 'react'; -import type {StyleProp, View, ViewStyle} from 'react-native'; -import {Keyboard, ScrollView} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native'; +import {Keyboard} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormElement from '@components/FormElement'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; +import ScrollView from '@components/ScrollView'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -60,7 +62,7 @@ function FormWrapper({ disablePressOnEnter = true, }: FormWrapperProps) { const styles = useThemeStyles(); - const formRef = useRef(null); + const formRef = useRef(null); const formContentRef = useRef(null); const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]); diff --git a/src/components/FormScrollView.tsx b/src/components/FormScrollView.tsx index ade167e9e628..91f5a825a38a 100644 --- a/src/components/FormScrollView.tsx +++ b/src/components/FormScrollView.tsx @@ -1,15 +1,16 @@ import type {ForwardedRef} from 'react'; import React from 'react'; -import type {ScrollViewProps} from 'react-native'; -import {ScrollView} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {ScrollView as RNScrollView, ScrollViewProps} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import ScrollView from './ScrollView'; type FormScrollViewProps = ScrollViewProps & { /** Form elements */ children: React.ReactNode; }; -function FormScrollView({children, ...rest}: FormScrollViewProps, ref: ForwardedRef) { +function FormScrollView({children, ...rest}: FormScrollViewProps, ref: ForwardedRef) { const styles = useThemeStyles(); return ( (null); - const transferBalanceButtonRef = useRef(null); + const anchorRef = useRef(null); + const transferBalanceButtonRef = useRef(null); const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); @@ -111,7 +111,7 @@ function KYCWall({ return; } - const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current); + const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current as HTMLDivElement); const position = getAnchorPosition(buttonPosition); setPositionAddPaymentMenu(position); @@ -162,7 +162,7 @@ function KYCWall({ } // Use event target as fallback if anchorRef is null for safety - const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLElement); + const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLDivElement); transferBalanceButtonRef.current = targetElement; @@ -181,7 +181,7 @@ function KYCWall({ return; } - const clickedElementLocation = getClickedTargetLocation(targetElement); + const clickedElementLocation = getClickedTargetLocation(targetElement as HTMLDivElement); const position = getAnchorPosition(clickedElementLocation); setPositionAddPaymentMenu(position); diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 102f85ea49b9..5784be21bac3 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -88,7 +88,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; + const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index fa6de1c2e4f4..68114dcf4e4c 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -217,11 +217,12 @@ function MoneyRequestConfirmationList(props) { const {onSendMoney, onConfirm, onSelectParticipant} = props; const {translate, toLocaleDigit} = useLocalize(); const transaction = props.transaction; - const {canUseViolations} = usePermissions(); + const {canUseP2PDistanceRequests, 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 canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill); const isSplitWithScan = isSplitBill && props.isScanRequest; @@ -721,13 +722,14 @@ function MoneyRequestConfirmationList(props) { )} {props.isDistanceRequest && ( Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - disabled={didConfirm || !isTypeRequest} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + disabled={didConfirm || !canEditDistance} interactive={!props.isReadOnly} /> )} diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index fe8cc3506b3f..7f9ab3fe0dc9 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -79,16 +78,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); - const isRequestModifiable = !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); - const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); - let canDeleteRequest = canModifyRequest; + const isDeletedParentAction = ReportActionsUtils.isDeletedAction(parentReportAction); + const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction; - if (ReportUtils.isPaidGroupPolicyExpenseReport(moneyRequestReport)) { - // If it's a paid policy expense report, only allow deleting the request if it's in draft state or instantly submitted state or the user is the policy admin - canDeleteRequest = - canDeleteRequest && - (ReportUtils.isDraftExpenseReport(moneyRequestReport) || ReportUtils.isExpenseReportWithInstantSubmittedState(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy)); - } + // If the report supports adding transactions to it, then it also supports deleting transactions from it. + const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction; const changeMoneyRequestStatus = () => { if (isOnHold) { @@ -108,7 +102,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, }, [canDeleteRequest]); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; - if (isRequestModifiable) { + if (canHoldOrUnholdRequest) { const isRequestIOU = parentReport?.type === 'iou'; const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU; const canModifyStatus = isPolicyAdmin || isActionOwner || isApprover; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 74a480a2eff7..968e1dfbfdca 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,6 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import Str from 'expensify-common/lib/str'; +import {isUndefined} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; @@ -245,11 +246,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const {canUseViolations} = usePermissions(); + const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit); const {unit, rate, currency} = mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); @@ -490,6 +492,31 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.setMoneyRequestMerchant(transaction.transactionID, distanceMerchant, true); }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]); + // Auto select the category if there is only one enabled category and it is required + useEffect(() => { + const enabledCategories = _.filter(policyCategories, (category) => category.enabled); + if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { + return; + } + IOU.setMoneyRequestCategory(transaction.transactionID, enabledCategories[0].name); + }, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]); + + // Auto select the tag if there is only one enabled tag and it is required + useEffect(() => { + let updatedTagsString = TransactionUtils.getTag(transaction); + policyTagLists.forEach((tagList, index) => { + const enabledTags = _.filter(tagList.tags, (tag) => tag.enabled); + const isTagListRequired = isUndefined(tagList.required) ? false : tagList.required && canUseViolations; + if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) { + return; + } + updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index); + }); + if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { + IOU.setMoneyRequestTag(transaction.transactionID, updatedTagsString); + } + }, [policyTagLists, transaction, policyTags, isTagRequired, canUseViolations]); + /** * @param {Object} option */ @@ -689,13 +716,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm || !isTypeRequest} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + disabled={didConfirm || !canEditDistance} interactive={!isReadOnly} /> ), diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index d14aec90fa10..0bc9130ea4a8 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -8,8 +8,8 @@ import createOnyxContext from './createOnyxContext'; const [withNetwork, NetworkProvider, NetworkContext] = createOnyxContext(ONYXKEYS.NETWORK); const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST); const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE); -const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS); -const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); +const [withReportActionsDrafts, ReportActionsDraftsProvider, , useReportActionsDrafts] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS); +const [withBlockedFromConcierge, BlockedFromConciergeProvider, , useBlockedFromConcierge] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); const [withBetas, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS); const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME); @@ -66,5 +66,7 @@ export { useFrequentlyUsedEmojis, withPreferredEmojiSkinTone, PreferredEmojiSkinToneContext, + useBlockedFromConcierge, + useReportActionsDrafts, useSession, }; diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index c0258f1252ef..40fb1115ac36 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import _ from 'underscore'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; @@ -9,6 +9,7 @@ import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; import OptionsList from '@components/OptionsList'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; +import ScrollView from '@components/ScrollView'; import ShowMoreButton from '@components/ShowMoreButton'; import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js index 10596bb9faf9..97d893b511dd 100644 --- a/src/components/PDFView/PDFPasswordForm.js +++ b/src/components/PDFView/PDFPasswordForm.js @@ -1,8 +1,9 @@ import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useRef, useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import _ from 'underscore'; import Button from '@components/Button'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 1bee95532104..c86d3b71c1d9 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,6 +1,7 @@ import lodashDefer from 'lodash/defer'; import type {ForwardedRef, ReactElement, ReactNode, RefObject} from 'react'; import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +// eslint-disable-next-line no-restricted-imports import type {ScrollView} from 'react-native'; import {View} from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index e06037f47b63..314c1ba141c3 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -1,5 +1,6 @@ import type {RefObject} from 'react'; -import type {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {Text, View} from 'react-native'; import type {PopoverAnchorPosition} from '@components/Modal/types'; import type BaseModalProps from '@components/Modal/types'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; @@ -20,7 +21,7 @@ type PopoverProps = BaseModalProps & anchorAlignment?: AnchorAlignment; /** The anchor ref of the popover */ - anchorRef: RefObject; + anchorRef: RefObject; /** Whether disable the animations */ disableAnimation?: boolean; diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx index 67481b41d50b..cc6c84477525 100644 --- a/src/components/PopoverProvider/index.tsx +++ b/src/components/PopoverProvider/index.tsx @@ -1,6 +1,7 @@ import type {RefObject} from 'react'; import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {Text, View} from 'react-native'; import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types'; const PopoverContext = createContext({ @@ -10,7 +11,7 @@ const PopoverContext = createContext({ isOpen: false, }); -function elementContains(ref: RefObject | undefined, target: EventTarget | null) { +function elementContains(ref: RefObject | undefined, target: EventTarget | null) { if (ref?.current && 'contains' in ref.current && ref?.current?.contains(target as Node)) { return true; } @@ -21,7 +22,7 @@ function PopoverContextProvider(props: PopoverContextProps) { const [isOpen, setIsOpen] = useState(false); const activePopoverRef = useRef(null); - const closePopover = useCallback((anchorRef?: RefObject): boolean => { + const closePopover = useCallback((anchorRef?: RefObject): boolean => { if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) { return false; } diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts index 2a366ae2a712..5022aee0f843 100644 --- a/src/components/PopoverProvider/types.ts +++ b/src/components/PopoverProvider/types.ts @@ -1,5 +1,6 @@ import type {ReactNode, RefObject} from 'react'; -import type {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {Text, View} from 'react-native'; type PopoverContextProps = { children: ReactNode; @@ -8,14 +9,14 @@ type PopoverContextProps = { type PopoverContextValue = { onOpen?: (popoverParams: AnchorRef) => void; popover?: AnchorRef | Record | null; - close: (anchorRef?: RefObject) => void; + close: (anchorRef?: RefObject) => void; isOpen: boolean; }; type AnchorRef = { - ref: RefObject; - close: (anchorRef?: RefObject) => void; - anchorRef: RefObject; + ref: RefObject; + close: (anchorRef?: RefObject) => void; + anchorRef: RefObject; }; export type {PopoverContextProps, PopoverContextValue, AnchorRef}; diff --git a/src/components/PopoverWithoutOverlay/types.ts b/src/components/PopoverWithoutOverlay/types.ts index 0d24cdd4bd9f..8fe40119ca61 100644 --- a/src/components/PopoverWithoutOverlay/types.ts +++ b/src/components/PopoverWithoutOverlay/types.ts @@ -1,5 +1,6 @@ import type {RefObject} from 'react'; -import type {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {Text, View} from 'react-native'; import type BaseModalProps from '@components/Modal/types'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -14,7 +15,7 @@ type PopoverWithoutOverlayProps = ChildrenProps & }; /** The anchor ref of the popover */ - anchorRef: RefObject; + anchorRef: RefObject; /** A react-native-animatable animation timing for the modal display animation */ animationInTiming?: number; diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index 2dd2e17e0454..9040a844e5a7 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -1,5 +1,6 @@ import type {ElementRef, ForwardedRef, RefObject} from 'react'; -import type {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, View, ViewStyle} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, Text as RNText, StyleProp, View, ViewStyle} from 'react-native'; import type {ValueOf} from 'type-fest'; import type {Shortcut} from '@libs/KeyboardShortcut'; import type CONST from '@src/CONST'; @@ -138,7 +139,7 @@ type PressableProps = RNPressableProps & noDragArea?: boolean; }; -type PressableRef = ForwardedRef; +type PressableRef = ForwardedRef; export default PressableProps; export type {PressableRef}; diff --git a/src/components/PressableWithSecondaryInteraction/types.ts b/src/components/PressableWithSecondaryInteraction/types.ts index aa67d45d66fb..b07c867daeb3 100644 --- a/src/components/PressableWithSecondaryInteraction/types.ts +++ b/src/components/PressableWithSecondaryInteraction/types.ts @@ -4,7 +4,7 @@ import type {ParsableStyle} from '@styles/utils/types'; type PressableWithSecondaryInteractionProps = PressableWithFeedbackProps & { /** The function that should be called when this pressable is pressed */ - onPress: (event?: GestureResponderEvent) => void; + onPress?: (event?: GestureResponderEvent) => void; /** The function that should be called when this pressable is pressedIn */ onPressIn?: (event?: GestureResponderEvent) => void; diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.tsx b/src/components/Reactions/ReportActionItemEmojiReactions.tsx index 7e95ab670b7e..c6bf4f9e4016 100644 --- a/src/components/Reactions/ReportActionItemEmojiReactions.tsx +++ b/src/components/Reactions/ReportActionItemEmojiReactions.tsx @@ -23,7 +23,7 @@ type ReportActionItemEmojiReactionsProps = WithCurrentUserPersonalDetailsProps & emojiReactions: OnyxEntry; /** The user's preferred locale. */ - preferredLocale: OnyxEntry; + preferredLocale?: OnyxEntry; /** The report action that these reactions are for */ reportAction: ReportAction; @@ -155,7 +155,7 @@ function ReportActionItemEmojiReactions({ shouldDisableOpacity={!!reportAction.pendingAction} > (popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref)} + ref={(ref) => (popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref ?? null)} count={reaction.reactionCount} emojiCodes={reaction.emojiCodes} onPress={reaction.onPress} diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index f0cd8dc1b4b5..60dbfc07966a 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -2,6 +2,7 @@ import Str from 'expensify-common/lib/str'; import React, {useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -26,7 +27,7 @@ type MoneyReportViewProps = { report: Report; /** Policy that the report belongs to */ - policy: Policy; + policy: OnyxEntry; /** Policy report fields */ policyReportFields: PolicyReportField[]; @@ -67,107 +68,111 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont - {ReportUtils.reportFieldsEnabled(report) && - sortedPolicyReportFields.map((reportField) => { - const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField); - const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue; - const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy); - - return ( - - Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))} - shouldShowRightIcon - disabled={isFieldDisabled} - wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} - shouldGreyOutWhenDisabled={false} - numberOfLinesTitle={0} - interactive - shouldStackHorizontally={false} - onSecondaryInteraction={() => {}} - hoverAndPressStyle={false} - titleWithTooltips={[]} - /> - - ); - })} - - - - {translate('common.total')} - - - - {isSettled && ( - - - - )} - - {formattedTotalAmount} - - - - {Boolean(shouldShowBreakdown) && ( + {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && ( <> - - - - {translate('cardTransactions.outOfPocket')} - - - - - {formattedOutOfPocketAmount} - - - - + {ReportUtils.reportFieldsEnabled(report) && + sortedPolicyReportFields.map((reportField) => { + const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField); + const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue; + const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy); + + return ( + + Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))} + shouldShowRightIcon + disabled={isFieldDisabled} + wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} + shouldGreyOutWhenDisabled={false} + numberOfLinesTitle={0} + interactive + shouldStackHorizontally={false} + onSecondaryInteraction={() => {}} + hoverAndPressStyle={false} + titleWithTooltips={[]} + /> + + ); + })} + - {translate('cardTransactions.companySpend')} + {translate('common.total')} + {isSettled && ( + + + + )} - {formattedCompanySpendAmount} + {formattedTotalAmount} + {Boolean(shouldShowBreakdown) && ( + <> + + + + {translate('cardTransactions.outOfPocket')} + + + + + {formattedOutOfPocketAmount} + + + + + + + {translate('cardTransactions.companySpend')} + + + + + {formattedCompanySpendAmount} + + + + + )} + )} - ); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 34d039153de7..1d5d443d3761 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -24,7 +24,6 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; @@ -474,8 +473,8 @@ export default withOnyx { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); + key: ({report, parentReportActions}) => { + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '']; const originalMessage = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage : undefined; const transactionID = originalMessage?.IOUTransactionID ?? 0; return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; diff --git a/src/components/ReportActionItem/TaskAction.tsx b/src/components/ReportActionItem/TaskAction.tsx index b10be4e86fe8..7e9262bb4c05 100644 --- a/src/components/ReportActionItem/TaskAction.tsx +++ b/src/components/ReportActionItem/TaskAction.tsx @@ -1,20 +1,24 @@ import React from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import * as TaskUtils from '@libs/TaskUtils'; +import type {ReportAction} from '@src/types/onyx'; type TaskActionProps = { /** Name of the reportAction action */ - actionName: string; + action: OnyxEntry; }; -function TaskAction({actionName}: TaskActionProps) { +function TaskAction({action}: TaskActionProps) { const styles = useThemeStyles(); + const message = TaskUtils.getTaskReportActionMessage(action); return ( - {TaskUtils.getTaskReportActionMessage(actionName)} + {message.html ? ${message.html}`} /> : {message.text}} ); } diff --git a/src/components/ScrollView.tsx b/src/components/ScrollView.tsx new file mode 100644 index 000000000000..a61c592015ee --- /dev/null +++ b/src/components/ScrollView.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import type {ForwardedRef} from 'react'; +// eslint-disable-next-line no-restricted-imports +import {ScrollView as RNScrollView} from 'react-native'; +import type {ScrollViewProps} from 'react-native'; + +function ScrollView({children, scrollIndicatorInsets, ...props}: ScrollViewProps, ref: ForwardedRef) { + return ( + + {children} + + ); +} + +ScrollView.displayName = 'ScrollView'; + +export type {ScrollViewProps}; + +export default React.forwardRef(ScrollView); diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx index 1ac53651a542..1b9bb2c09f56 100644 --- a/src/components/ScrollViewWithContext.tsx +++ b/src/components/ScrollViewWithContext.tsx @@ -1,13 +1,14 @@ import type {ForwardedRef, ReactNode} from 'react'; import React, {createContext, forwardRef, useMemo, useRef, useState} from 'react'; -import type {NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps} from 'react-native'; -import {ScrollView} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {NativeScrollEvent, NativeSyntheticEvent, ScrollView as RNScrollView, ScrollViewProps} from 'react-native'; +import ScrollView from './ScrollView'; const MIN_SMOOTH_SCROLL_EVENT_THROTTLE = 16; type ScrollContextValue = { contentOffsetY: number; - scrollViewRef: ForwardedRef; + scrollViewRef: ForwardedRef; }; const ScrollContext = createContext({ @@ -28,9 +29,9 @@ type ScrollViewWithContextProps = Partial & { * Using this wrapper will automatically handle scrolling to the picker's * when the picker modal is opened */ -function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { +function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { const [contentOffsetY, setContentOffsetY] = useState(0); - const defaultScrollViewRef = useRef(null); + const defaultScrollViewRef = useRef(null); const scrollViewRef = ref ?? defaultScrollViewRef; const setContextScrollPosition = (event: NativeSyntheticEvent) => { diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js index 23d0bb6f816b..92d829e9d0db 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.js +++ b/src/components/VideoPlayer/BaseVideoPlayer.js @@ -82,7 +82,7 @@ function BaseVideoPlayer({ setIsPopoverVisible(false); }; - // fix for iOS mWeb: preventing iOS native player edfault behavior from pausing the video when exiting fullscreen + // fix for iOS mWeb: preventing iOS native player default behavior from pausing the video when exiting fullscreen const preventPausingWhenExitingFullscreen = useCallback( (isVideoPlaying) => { if (videoResumeTryNumber.current === 0 || isVideoPlaying) { @@ -121,6 +121,7 @@ function BaseVideoPlayer({ const handleFullscreenUpdate = useCallback( (e) => { onFullscreenUpdate(e); + // fix for iOS native and mWeb: when switching to fullscreen and then exiting // the fullscreen mode while playing, the video pauses if (!isPlaying || e.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) { @@ -139,7 +140,8 @@ function BaseVideoPlayer({ const bindFunctions = useCallback(() => { currentVideoPlayerRef.current._onPlaybackStatusUpdate = handlePlaybackStatusUpdate; currentVideoPlayerRef.current._onFullscreenUpdate = handleFullscreenUpdate; - // update states after binding + + // Update states after binding currentVideoPlayerRef.current.getStatusAsync().then((status) => { handlePlaybackStatusUpdate(status); }); @@ -149,6 +151,7 @@ function BaseVideoPlayer({ if (!isUploading) { return; } + // If we are uploading a new video, we want to immediately set the video player ref. currentVideoPlayerRef.current = videoPlayerRef.current; }, [url, currentVideoPlayerRef, isUploading]); @@ -162,6 +165,7 @@ function BaseVideoPlayer({ if (shouldUseSharedVideoElementRef.current) { return; } + // If it's not a shared video player, clear the video player ref. currentVideoPlayerRef.current = null; }, diff --git a/src/components/createOnyxContext.tsx b/src/components/createOnyxContext.tsx index 50cda00b17b4..c19b8006c86c 100644 --- a/src/components/createOnyxContext.tsx +++ b/src/components/createOnyxContext.tsx @@ -3,7 +3,7 @@ import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, PropsWithou import React, {createContext, forwardRef, useContext} from 'react'; import {withOnyx} from 'react-native-onyx'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -import type {OnyxKey, OnyxValue, OnyxValues} from '@src/ONYXKEYS'; +import type {OnyxKey, OnyxValue} from '@src/ONYXKEYS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; // Provider types @@ -32,11 +32,11 @@ type CreateOnyxContext = [ WithOnyxKey, ComponentType, TOnyxKey>>, React.Context>, - () => OnyxValues[TOnyxKey], + () => NonNullable>, ]; export default (onyxKeyName: TOnyxKey): CreateOnyxContext => { - const Context = createContext>(null); + const Context = createContext>(null as OnyxValue); function Provider(props: ProviderPropsWithOnyx): ReactNode { return {props.children}; } @@ -86,7 +86,7 @@ export default (onyxKeyName: TOnyxKey): CreateOnyxCont if (value === null) { throw new Error(`useOnyxContext must be used within a OnyxProvider [key: ${onyxKeyName}]`); } - return value; + return value as NonNullable>; }; return [withOnyxKey, ProviderWithOnyx, Context, useOnyxContext]; diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a52cca62ef5..3575854ee7e2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1754,6 +1754,7 @@ export default { workspaceType: 'Workspace type', workspaceAvatar: 'Workspace avatar', mustBeOnlineToViewMembers: 'You must be online in order to view members of this workspace.', + requested: 'Requested', }, type: { free: 'Free', @@ -1770,6 +1771,10 @@ export default { subtitle: 'Add a category to organize your spend.', }, genericFailureMessage: 'An error occurred while updating the category, please try again.', + addCategory: 'Add category', + categoryRequiredError: 'Category name is required.', + existingCategoryError: 'A category with this name already exists.', + invalidCategoryName: 'Invalid category name.', }, tags: { requiresTag: 'Members must tag all spend', @@ -1805,6 +1810,9 @@ export default { genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.', removeMembersPrompt: 'Are you sure you want to remove these members?', removeMembersTitle: 'Remove members', + removeMemberButtonTitle: 'Remove from workspace', + removeMemberPrompt: ({memberName}) => `Are you sure you want to remove ${memberName}`, + removeMemberTitle: 'Remove member', makeMember: 'Make member', makeAdmin: 'Make admin', selectAll: 'Select all', @@ -2196,6 +2204,7 @@ export default { viewAttachment: 'View attachment', }, parentReportAction: { + deletedReport: '[Deleted report]', deletedMessage: '[Deleted message]', deletedRequest: '[Deleted request]', reversedTransaction: '[Reversed transaction]', @@ -2239,6 +2248,10 @@ export default { invite: 'Invite them', nothing: 'Do nothing', }, + actionableMentionJoinWorkspaceOptions: { + accept: 'Accept', + decline: 'Decline', + }, teachersUnitePage: { teachersUnite: 'Teachers Unite', joinExpensifyOrg: 'Join Expensify.org in eliminating injustice around the world and help teachers split their expenses for classrooms in need!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 013255c1e11e..51a83e55fee2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1778,6 +1778,7 @@ export default { workspaceType: 'Tipo de espacio de trabajo', workspaceAvatar: 'Espacio de trabajo avatar', mustBeOnlineToViewMembers: 'Debes estar en línea para poder ver los miembros de este espacio de trabajo.', + requested: 'Solicitado', }, type: { free: 'Gratis', @@ -1794,6 +1795,10 @@ export default { subtitle: 'Añade una categoría para organizar tu gasto.', }, genericFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.', + addCategory: 'Añadir categoría', + categoryRequiredError: 'Lo nombre de la categoría es obligatorio.', + existingCategoryError: 'Ya existe una categoría con este nombre.', + invalidCategoryName: 'Lo nombre de la categoría es invalido.', }, tags: { requiresTag: 'Los miembros deben etiquetar todos los gastos', @@ -1829,6 +1834,9 @@ export default { genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor, inténtalo más tarde.', removeMembersPrompt: '¿Estás seguro de que deseas eliminar a estos miembros?', removeMembersTitle: 'Eliminar miembros', + removeMemberButtonTitle: 'Quitar del espacio de trabajo', + removeMemberPrompt: ({memberName}) => `¿Estás seguro de que deseas eliminar a ${memberName}`, + removeMemberTitle: 'Eliminar miembro', makeMember: 'Hacer miembro', makeAdmin: 'Hacer administrador', selectAll: 'Seleccionar todo', @@ -2684,6 +2692,7 @@ export default { viewAttachment: 'Ver archivo adjunto', }, parentReportAction: { + deletedReport: '[Informe eliminado]', deletedMessage: '[Mensaje eliminado]', deletedRequest: '[Solicitud eliminada]', reversedTransaction: '[Transacción anulada]', @@ -2705,6 +2714,10 @@ export default { invite: 'Invitar', nothing: 'No hacer nada', }, + actionableMentionJoinWorkspaceOptions: { + accept: 'Aceptar', + decline: 'Rechazar', + }, moderation: { flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.', chooseAReason: 'Elige abajo un motivo para reportarlo:', diff --git a/src/libs/API/parameters/AcceptJoinRequest.ts b/src/libs/API/parameters/AcceptJoinRequest.ts new file mode 100644 index 000000000000..4c7b6a00b2fb --- /dev/null +++ b/src/libs/API/parameters/AcceptJoinRequest.ts @@ -0,0 +1,5 @@ +type AcceptJoinRequestParams = { + requests: string; +}; + +export default AcceptJoinRequestParams; diff --git a/src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts b/src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts new file mode 100644 index 000000000000..629a66c2e657 --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts @@ -0,0 +1,10 @@ +type CreateWorkspaceCategoriesParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array<{name: string;}> + */ + categories: string; +}; + +export default CreateWorkspaceCategoriesParams; diff --git a/src/libs/API/parameters/DeclineJoinRequest.ts b/src/libs/API/parameters/DeclineJoinRequest.ts new file mode 100644 index 000000000000..da0b147254d8 --- /dev/null +++ b/src/libs/API/parameters/DeclineJoinRequest.ts @@ -0,0 +1,5 @@ +type DeclineJoinRequestParams = { + requests: string; +}; + +export default DeclineJoinRequestParams; diff --git a/src/libs/API/parameters/JoinPolicyInviteLink.ts b/src/libs/API/parameters/JoinPolicyInviteLink.ts new file mode 100644 index 000000000000..4b280b8cd8c6 --- /dev/null +++ b/src/libs/API/parameters/JoinPolicyInviteLink.ts @@ -0,0 +1,6 @@ +type JoinPolicyInviteLinkParams = { + policyID: string; + inviterEmail: string; +}; + +export default JoinPolicyInviteLinkParams; diff --git a/src/libs/API/parameters/PayMoneyRequestParams.ts b/src/libs/API/parameters/PayMoneyRequestParams.ts index edf05b6ce528..4a769f057e10 100644 --- a/src/libs/API/parameters/PayMoneyRequestParams.ts +++ b/src/libs/API/parameters/PayMoneyRequestParams.ts @@ -5,6 +5,7 @@ type PayMoneyRequestParams = { chatReportID: string; reportActionID: string; paymentMethodType: PaymentMethodType; + amount?: number; }; export default PayMoneyRequestParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 00e8b5e761ad..f529032130bb 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -149,9 +149,13 @@ export type {default as AcceptACHContractForBankAccount} from './AcceptACHContra export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspaceDescriptionParams'; export type {default as UpdateWorkspaceMembersRoleParams} from './UpdateWorkspaceMembersRoleParams'; export type {default as SetWorkspaceCategoriesEnabledParams} from './SetWorkspaceCategoriesEnabledParams'; +export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspaceCategoriesParams'; export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams'; export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams'; export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams'; export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; +export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest'; +export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest'; +export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index ee4ce1ea3670..1b41ced4f1d7 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -115,6 +115,7 @@ const WRITE_COMMANDS = { CREATE_WORKSPACE: 'CreateWorkspace', CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled', + CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories', SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory', CREATE_TASK: 'CreateTask', CANCEL_TASK: 'CancelTask', @@ -156,6 +157,9 @@ const WRITE_COMMANDS = { CANCEL_PAYMENT: 'CancelPayment', ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount', SWITCH_TO_OLD_DOT: 'SwitchToOldDot', + JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink', + ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest', + DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', } as const; type WriteCommand = ValueOf; @@ -264,6 +268,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams; [WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams; [WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams; + [WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams; [WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams; @@ -310,6 +315,9 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; + [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; + [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; + [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; }; const READ_COMMANDS = { @@ -386,6 +394,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { OPEN_OLD_DOT_LINK: 'OpenOldDotLink', REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails', GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages', + JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink', RECONNECT_APP: 'ReconnectApp', } as const; @@ -397,6 +406,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: Parameters.OpenOldDotLinkParams; [SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: Parameters.RevealExpensifyCardDetailsParams; [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams; + [SIDE_EFFECT_REQUEST_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; }; diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index a42cb6a8f756..aef615018b4c 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -7,6 +7,7 @@ import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; type DefaultMileageRate = { + customUnitRateID?: string; rate?: number; currency?: string; unit: Unit; @@ -38,6 +39,7 @@ function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | } return { + customUnitRateID: distanceRate.customUnitRateID, rate: distanceRate.rate, currency: distanceRate.currency, unit: distanceUnit.attributes.unit, @@ -76,6 +78,27 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string return convertedDistance.toFixed(2); } +/** + * @param hasRoute Whether the route exists for the distance request + * @param distanceInMeters Distance traveled + * @param unit Unit that should be used to display the distance + * @param rate Expensable amount allowed per unit + * @param translate Translate function + * @returns A string that describes the distance traveled + */ +function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, rate: number, translate: LocaleContextProps['translate']): string { + if (!hasRoute || !rate) { + return translate('iou.routePending'); + } + + const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); + const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); + const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); + const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; + + return `${distanceInUnits} ${unitString}`; +} + /** * @param hasRoute Whether the route exists for the distance request * @param distanceInMeters Distance traveled @@ -99,15 +122,13 @@ function getDistanceMerchant( return translate('iou.routePending'); } - const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); - const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); + const formattedDistance = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); - const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; const ratePerUnit = PolicyUtils.getUnitRateValue({rate}, toLocaleDigit); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const currencySymbol = CurrencyUtils.getCurrencySymbol(currency) || `${currency} `; - return `${distanceInUnits} ${unitString} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; + return `${formattedDistance} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; } /** diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index cab0f48d75fd..33cda171f24b 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -242,9 +242,13 @@ function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji /** * Given an emoji item object, return an emoji code based on its type. */ -const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: number): string => { +const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: OnyxEntry): string | undefined => { const {code, types} = item; - if (types?.[preferredSkinToneIndex]) { + if (!preferredSkinToneIndex) { + return; + } + + if (typeof preferredSkinToneIndex === 'number' && types?.[preferredSkinToneIndex]) { return types[preferredSkinToneIndex]; } @@ -305,7 +309,7 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[] * Replace any emoji name in a text with the emoji icon. * If we're on mobile, we also add a space after the emoji granted there's no text after it. */ -function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; @@ -345,9 +349,9 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF // Set the cursor to the end of the last replaced Emoji. Note that we position after // the extra space, if we added one. - cursorPosition = newText.indexOf(emoji) + emojiReplacement.length; + cursorPosition = newText.indexOf(emoji) + (emojiReplacement?.length ?? 0); - newText = newText.replace(emoji, emojiReplacement); + newText = newText.replace(emoji, emojiReplacement ?? ''); } } @@ -369,7 +373,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, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); return { diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 20313ee8912d..784d339a4a0d 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -180,3 +180,5 @@ export { getMicroSecondOnyxErrorObject, isReceiptError, }; + +export type {OnyxDataWithErrors}; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index fc89b53fbefd..6f5dcdf9cda9 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -63,6 +63,7 @@ const loadConciergePage = () => require('../../../pages/ConciergePage').default const loadProfileAvatar = () => require('../../../pages/settings/Profile/ProfileAvatar').default as React.ComponentType; const loadWorkspaceAvatar = () => require('../../../pages/workspace/WorkspaceAvatar').default as React.ComponentType; const loadReportAvatar = () => require('../../../pages/ReportAvatar').default as React.ComponentType; +const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default as React.ComponentType; let timezone: Timezone | null; let currentAccountID = -1; @@ -356,6 +357,14 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f options={screenOptions.fullScreen} component={DesktopSignInRedirectPage} /> + ); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 545641957c9a..978e338796ea 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -251,6 +251,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MEMBER_DETAILS]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType, diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index f38ec213a466..58d9efb43df5 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -11,6 +11,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as Session from '@libs/actions/Session'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -47,7 +48,8 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps // When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method. // To prevent this, the value of the bottomTabRoute?.name is checked here bottomTabRoute?.name === SCREENS.WORKSPACE.INITIAL || - (currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) + Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || + Session.isAnonymousUser() ) { return; } diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 20c426a74c71..2ca4c5178a5e 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -45,7 +45,7 @@ function parseAndLogRoute(state: NavigationState) { const focusedRoute = findFocusedRoute(state); - if (focusedRoute?.name !== SCREENS.NOT_FOUND) { + if (focusedRoute?.name !== SCREENS.NOT_FOUND && focusedRoute?.name !== SCREENS.SAML_SIGN_IN) { updateLastVisitedPath(currentPath); } diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 7959999ee813..5bc7d52230a8 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -4,9 +4,9 @@ import SCREENS from '@src/SCREENS'; const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = { [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE], [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT], - [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE], + [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION], [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], - [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], + [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 3ceb3c1ac7df..8a24dc177a80 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -22,6 +22,7 @@ const config: LinkingOptions['config'] = { [SCREENS.PROFILE_AVATAR]: ROUTES.PROFILE_AVATAR.route, [SCREENS.WORKSPACE_AVATAR]: ROUTES.WORKSPACE_AVATAR.route, [SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route, + [SCREENS.WORKSPACE_JOIN_USER]: ROUTES.WORKSPACE_JOIN_USER.route, // Sidebar [NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: { @@ -280,6 +281,15 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route, }, + [SCREENS.WORKSPACE.MEMBER_DETAILS]: { + path: ROUTES.WORKSPACE_MEMBER_DETAILS.route, + }, + [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: { + path: ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.route, + }, + [SCREENS.WORKSPACE.CATEGORY_CREATE]: { + path: ROUTES.WORKSPACE_CATEGORY_CREATE.route, + }, [SCREENS.REIMBURSEMENT_ACCOUNT]: { path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 6790dd5f8f10..decb905ac52f 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -93,6 +93,7 @@ type CentralPaneNavigatorParamList = { }; [SCREENS.WORKSPACE.TAGS]: { policyID: string; + categoryName: string; }; }; @@ -197,6 +198,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.INVITE_MESSAGE]: { policyID: string; }; + [SCREENS.WORKSPACE.CATEGORY_CREATE]: { + policyID: string; + }; [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: { policyID: string; categoryName: string; @@ -204,6 +208,16 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.MEMBER_DETAILS]: { + policyID: string; + accountID: string; + backTo: Routes; + }; + [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: { + policyID: string; + accountID: string; + backTo: Routes; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; @@ -566,6 +580,10 @@ type AuthScreensParamList = SharedScreensParamList & { [SCREENS.WORKSPACE_AVATAR]: { policyID: string; }; + [SCREENS.WORKSPACE_JOIN_USER]: { + policyID: string; + email: string; + }; [SCREENS.REPORT_AVATAR]: { reportID: string; }; diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts index d6ec246eddf7..40778f38c0d4 100644 --- a/src/libs/Notification/PushNotification/NotificationType.ts +++ b/src/libs/Notification/PushNotification/NotificationType.ts @@ -18,6 +18,8 @@ type ReportCommentNotificationData = { shouldScrollToLastUnread?: boolean; roomName?: string; onyxData?: OnyxServerUpdate[]; + lastUpdateID?: number; + previousUpdateID?: number; }; /** diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 813e0aecbd5c..7f86d3ddb9ac 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import * as OnyxUpdates from '@libs/actions/OnyxUpdates'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils'; @@ -6,8 +7,10 @@ import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; import {doesReportBelongToWorkspace, getReport} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; import * as Modal from '@userActions/Modal'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import backgroundRefresh from './backgroundRefresh'; import PushNotification from './index'; @@ -27,9 +30,28 @@ Onyx.connect({ * Setup reportComment push notification callbacks. */ export default function subscribeToReportCommentPushNotifications() { - PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => { + PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => { Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); - Onyx.update(onyxData ?? []); + + if (onyxData && lastUpdateID && previousUpdateID) { + Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + + const updates: OnyxUpdatesFromServer = { + type: CONST.ONYX_UPDATE_TYPES.AIRSHIP, + lastUpdateID, + previousUpdateID, + updates: [ + { + eventType: 'eventType', + data: onyxData, + }, + ], + }; + OnyxUpdates.applyOnyxUpdatesReliably(updates); + } else { + Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + } + backgroundRefresh(); }); diff --git a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts index 9c7e6402d69b..82410b120df2 100644 --- a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts +++ b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts @@ -1,6 +1,7 @@ -import type {OnyxValue} from '@src/ONYXKEYS'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {Report} from '@src/types/onyx'; -export default function reportWithoutHasDraftSelector(report: OnyxValue<'report_'>) { +export default function reportWithoutHasDraftSelector(report: OnyxEntry) { if (!report) { return report; } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 07f0df962455..fd803a508b4a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -107,7 +107,7 @@ type Hierarchy = Record; selectedOptions?: Option[]; maxRecentReportsToShow?: number; excludeLogins?: string[]; @@ -156,7 +156,6 @@ type SectionForSearchTerm = { section: CategorySection; newIndexOffset: number; }; - type GetOptions = { recentReports: ReportUtils.OptionData[]; personalDetails: ReportUtils.OptionData[]; @@ -533,7 +532,6 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails // some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action const lastOriginalReportAction = lastReportActions[report?.reportID ?? ''] ?? null; let lastMessageTextFromReport = ''; - const lastActionName = lastReportAction?.actionName ?? ''; if (ReportUtils.isArchivedRoom(report)) { const archiveReason = @@ -585,12 +583,8 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(report?.reportID, lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); - } else if ( - lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || - lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED || - lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED - ) { - lastMessageTextFromReport = lastReportAction?.message?.[0].text ?? ''; + } else if (ReportActionUtils.isTaskAction(lastReportAction)) { + lastMessageTextFromReport = TaskUtils.getTaskReportActionMessage(lastReportAction).text; } else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) { lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction); } else if (ReportActionUtils.isApprovedOrSubmittedReportAction(lastReportAction)) { @@ -1441,7 +1435,8 @@ function getOptions( const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; - const doesReportHaveViolations = betas.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + const doesReportHaveViolations = + (betas?.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction)) ?? false; return ReportUtils.shouldReportBeInOptionList({ report, @@ -1805,7 +1800,7 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[], function getFilteredOptions( reports: OnyxCollection, personalDetails: OnyxEntry, - betas: Beta[] = [], + betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], excludeLogins: string[] = [], @@ -1852,9 +1847,9 @@ function getFilteredOptions( */ function getShareDestinationOptions( - reports: Record, + reports: Record, personalDetails: OnyxEntry, - betas: Beta[] = [], + betas: OnyxEntry = [], searchValue = '', selectedOptions: Array> = [], excludeLogins: string[] = [], diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index c9f386f5bd7a..26df03134fd5 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } +function canUseP2PDistanceRequests(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas); +} + function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas); } @@ -44,5 +48,6 @@ export default { canUseLinkPreviews, canUseViolations, canUseReportFields, + canUseP2PDistanceRequests, canUseWorkflowsDelayedSubmission, }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 5b916148c6ee..f6534e075773 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -91,7 +91,9 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMemb */ function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( - !!policy && policy?.isPolicyExpenseChatEnabled && (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) + !!policy && + (policy?.isPolicyExpenseChatEnabled || Boolean(policy?.isJoinRequestPending)) && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) ); } @@ -227,7 +229,7 @@ function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean { * Checks if policy's scheduled submit / auto reporting frequency is "instant". * Note: Free policies have "instant" submit always enabled. */ -function isInstantSubmitEnabled(policy: OnyxEntry): boolean { +function isInstantSubmitEnabled(policy: OnyxEntry | EmptyObject): boolean { return policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT || policy?.type === CONST.POLICY.TYPE.FREE; } @@ -242,6 +244,13 @@ function extractPolicyIDFromPath(path: string) { return path.match(CONST.REGEX.POLICY_ID_FROM_PATH)?.[1]; } +/** + * Whether the policy has active accounting integration connections + */ +function hasAccountingConnections(policy: OnyxEntry) { + return Boolean(policy?.connections); +} + function getPathWithoutPolicyID(path: string) { return path.replace(CONST.REGEX.PATH_WITHOUT_POLICY_ID, '/'); } @@ -263,6 +272,7 @@ function goBackFromInvalidPolicy() { export { getActivePolicies, + hasAccountingConnections, hasPolicyMemberError, hasPolicyError, hasPolicyErrorFields, diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts index bc48111eadc5..3cb15c0f3fc3 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher.ts @@ -5,7 +5,7 @@ import type {LiteralUnion, ValueOf} from 'type-fest'; import Log from '@libs/Log'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxUpdateEvent, OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; +import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import TYPE from './EventType'; import Pusher from './library'; @@ -22,8 +22,6 @@ type Args = { authEndpoint: string; }; -type PushJSON = OnyxUpdateEvent[] | OnyxUpdatesFromServer; - type UserIsTypingEvent = ReportUserIsTyping & { userLogin?: string; }; @@ -37,7 +35,7 @@ type PusherEventMap = { [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent; }; -type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : PushJSON; +type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : OnyxUpdatesFromServer; type EventCallbackError = {type: ValueOf; data: {code: number}}; @@ -413,4 +411,4 @@ export { getPusherSocketID, }; -export type {EventCallbackError, States, PushJSON, UserIsTypingEvent, UserIsLeavingRoomEvent}; +export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent}; diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts index 1ee75eb9c2f6..2bd79adef516 100644 --- a/src/libs/PusherUtils.ts +++ b/src/libs/PusherUtils.ts @@ -1,10 +1,10 @@ import type {OnyxUpdate} from 'react-native-onyx'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import Log from './Log'; import NetworkConnection from './NetworkConnection'; import * as Pusher from './Pusher/pusher'; -import type {PushJSON} from './Pusher/pusher'; type Callback = (data: OnyxUpdate[]) => Promise; @@ -25,10 +25,10 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis /** * Abstraction around subscribing to private user channel events. Handles all logs and errors automatically. */ -function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: PushJSON) => void) { +function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer) => void) { const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}` as const; - function logPusherEvent(pushJSON: PushJSON) { + function logPusherEvent(pushJSON: OnyxUpdatesFromServer) { Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON); } @@ -36,7 +36,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string NetworkConnection.triggerReconnectionCallbacks('Pusher re-subscribed to private user channel'); } - function onEventPush(pushJSON: PushJSON) { + function onEventPush(pushJSON: OnyxUpdatesFromServer) { logPusherEvent(pushJSON); onEvent(pushJSON); } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 628d88fdc76f..3b1ecf45adb4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -6,7 +6,15 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ActionName, ChangeLog, IOUMessage, OriginalMessageActionableMentionWhisper, OriginalMessageIOU, OriginalMessageReimbursementDequeued} from '@src/types/onyx/OriginalMessage'; +import type { + ActionName, + ChangeLog, + IOUMessage, + OriginalMessageActionableMentionWhisper, + OriginalMessageIOU, + OriginalMessageJoinPolicyChangeLog, + OriginalMessageReimbursementDequeued, +} from '@src/types/onyx/OriginalMessage'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -205,7 +213,7 @@ function isTransactionThread(parentReportAction: OnyxEntry): boole ); } -function getOneTransactionThreadReportID(reportActions: ReportActions): string { +function getOneTransactionThreadReportID(reportActions: OnyxEntry): string { const reportActionsArray = Object.values(reportActions ?? {}); if (!reportActionsArray.length) { @@ -407,10 +415,6 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: return false; } - if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.TASKEDITED) { - return false; - } - // Filter out any unsupported reportAction types if (!supportedActionTypes.includes(reportAction.actionName)) { return false; @@ -715,7 +719,8 @@ function isTaskAction(reportAction: OnyxEntry): boolean { return ( reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || - reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED + reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED || + reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKEDITED ); } @@ -872,7 +877,7 @@ function hasRequestFromCurrentAccount(reportID: string, currentAccountID: number * Checks if a given report action corresponds to an actionable mention whisper. * @param reportAction */ -function isActionableMentionWhisper(reportAction: OnyxEntry): boolean { +function isActionableMentionWhisper(reportAction: OnyxEntry): reportAction is ReportActionBase & OriginalMessageActionableMentionWhisper { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEMENTIONWHISPER; } @@ -924,6 +929,26 @@ function isCurrentActionUnread(report: Report | EmptyObject, reportAction: Repor return isReportActionUnread(reportAction, lastReadTime) && (!prevReportAction || !isReportActionUnread(prevReportAction, lastReadTime)); } +/** + * Checks if a given report action corresponds to a join request action. + * @param reportAction + */ +function isActionableJoinRequest(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST; +} + +/** + * Checks if any report actions correspond to a join request action that is still pending. + * @param reportID + */ +function isActionableJoinRequestPending(reportID: string): boolean { + const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(reportID))); + const findPendingRequest = sortedReportActions.find( + (reportActionItem) => isActionableJoinRequest(reportActionItem) && (reportActionItem as OriginalMessageJoinPolicyChangeLog)?.originalMessage?.choice === '', + ); + return !!findPendingRequest; +} + function isApprovedOrSubmittedReportAction(action: OnyxEntry | EmptyObject) { return [CONST.REPORT.ACTIONS.TYPE.APPROVED, CONST.REPORT.ACTIONS.TYPE.SUBMITTED].some((type) => type === action?.actionName); } @@ -991,6 +1016,8 @@ export { isActionableMentionWhisper, getActionableMentionWhisperMessage, isCurrentActionUnread, + isActionableJoinRequest, + isActionableJoinRequestPending, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 393dfec86f18..29b401c92477 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -28,6 +28,7 @@ import type { ReportAction, ReportMetadata, Session, + Task, Transaction, TransactionViolation, } from '@src/types/onyx'; @@ -515,6 +516,14 @@ Onyx.connect({ }, }); +function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource { + return currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID); +} + +function getCurrentUserDisplayNameOrEmail(): string | undefined { + return currentUserPersonalDetails?.displayName ?? currentUserEmail; +} + function getChatType(report: OnyxEntry | Participant | EmptyObject): ValueOf | undefined { return report?.chatType; } @@ -959,14 +968,6 @@ function isProcessingReport(report: OnyxEntry | EmptyObject): boolean { return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED; } -/** - * Returns true if the policy has `instant` reporting frequency and if the report is still being processed (i.e. submitted state) - */ -function isExpenseReportWithInstantSubmittedState(report: OnyxEntry | EmptyObject): boolean { - const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`] ?? null; - return isExpenseReport(report) && isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy); -} - /** * Check if the report is a single chat report that isn't a thread * and personal detail of participant is optimistic data @@ -1075,6 +1076,20 @@ function findLastAccessedReport( return adminReport ?? sortedReports.at(-1) ?? null; } +/** + * Whether the provided report has expenses + */ +function hasExpenses(reportID?: string): boolean { + return !!Object.values(allTransactions ?? {}).find((transaction) => `${transaction?.reportID}` === `${reportID}`); +} + +/** + * Whether the provided report is a closed expense report with no expenses + */ +function isClosedExpenseReportWithNoExpenses(report: OnyxEntry): boolean { + return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && isExpenseReport(report) && !hasExpenses(report.reportID); +} + /** * Whether the provided report is an archived room */ @@ -1082,6 +1097,16 @@ function isArchivedRoom(report: OnyxEntry | EmptyObject): boolean { return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED; } +/** + * Whether the provided report is the admin's room + */ +function isJoinRequestInAdminRoom(report: OnyxEntry): boolean { + if (!report) { + return false; + } + return ReportActionsUtils.isActionableJoinRequestPending(report.reportID); +} + /** * Checks if the current user is allowed to comment on the given report. */ @@ -1296,6 +1321,29 @@ function getChildReportNotificationPreference(reportAction: OnyxEntry): boolean { + if (!isMoneyRequestReport(moneyRequestReport)) { + return false; + } + + if (isReportApproved(moneyRequestReport) || isSettled(moneyRequestReport?.reportID)) { + return false; + } + + if (isGroupPolicy(moneyRequestReport) && isProcessingReport(moneyRequestReport) && !PolicyUtils.isInstantSubmitEnabled(getPolicy(moneyRequestReport?.policyID))) { + return false; + } + + return true; +} + /** * Can only delete if the author is this user and the action is an ADDCOMMENT action or an IOU action in an unsettled report, or if the user is a * policy admin @@ -1310,14 +1358,13 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: // For now, users cannot delete split actions const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (!isEmptyObject(report) && isReportApproved(report))) { + if (isSplitAction) { return false; } if (isActionOwner) { - if (!isEmptyObject(report) && isPaidGroupPolicyExpenseReport(report)) { - // If it's a paid policy expense report, only allow deleting the request if it's a draft or is instantly submitted or the user is the policy admin - return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report) || PolicyUtils.isPolicyAdmin(policy); + if (!isEmptyObject(report) && isMoneyRequestReport(report)) { + return canAddOrDeleteTransactions(report); } return true; } @@ -1842,7 +1889,7 @@ function buildOptimisticCancelPaymentReportAction(expenseReportID: string, amoun person: [ { style: 'strong', - text: currentUserPersonalDetails?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), type: 'TEXT', }, ], @@ -1909,6 +1956,10 @@ function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | Op return false; } + if (isJoinRequestInAdminRoom(optionOrReport)) { + return true; + } + if (isArchivedRoom(optionOrReport) || isArchivedRoom(getReport(optionOrReport.parentReportID))) { return false; } @@ -2605,6 +2656,10 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu return parentReportActionMessage; } + if (isClosedExpenseReportWithNoExpenses(report)) { + return Localize.translateLocal('parentReportAction.deletedReport'); + } + if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { return Localize.translateLocal('parentReportAction.deletedTask'); } @@ -3005,11 +3060,10 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency); const policy = getPolicy(policyID); - const isFree = policy?.type === CONST.POLICY.TYPE.FREE; + const isInstantSubmitEnabled = PolicyUtils.isInstantSubmitEnabled(policy); - // Define the state and status of the report based on whether the policy is free or paid - const stateNum = isFree ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN; - const statusNum = isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN; + const stateNum = isInstantSubmitEnabled ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN; + const statusNum = isInstantSubmitEnabled ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN; const expenseReport: OptimisticExpenseReport = { reportID: generateReportID(), @@ -3180,14 +3234,14 @@ function buildOptimisticIOUReportAction( actionName: CONST.REPORT.ACTIONS.TYPE.IOU, actorAccountID: currentUserAccountID, automatic: false, - avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), isAttachment: false, originalMessage, message: getIOUReportActionMessage(iouReportID, type, amount, comment, currency, paymentType, isSettlingUp), person: [ { style: 'strong', - text: currentUserPersonalDetails?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), type: 'TEXT', }, ], @@ -3213,14 +3267,14 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, actorAccountID: currentUserAccountID, automatic: false, - avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), isAttachment: false, originalMessage, message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.APPROVED, Math.abs(amount), '', currency), person: [ { style: 'strong', - text: currentUserPersonalDetails?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), type: 'TEXT', }, ], @@ -3255,14 +3309,14 @@ function buildOptimisticMovedReportAction(fromPolicyID: string, toPolicyID: stri actionName: CONST.REPORT.ACTIONS.TYPE.MOVED, actorAccountID: currentUserAccountID, automatic: false, - avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), isAttachment: false, originalMessage, message: movedActionMessage, person: [ { style: 'strong', - text: currentUserPersonalDetails?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), type: 'TEXT', }, ], @@ -3288,14 +3342,14 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, actorAccountID: currentUserAccountID, automatic: false, - avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), isAttachment: false, originalMessage, message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.SUBMITTED, Math.abs(amount), '', currency), person: [ { style: 'strong', - text: currentUserPersonalDetails?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), type: 'TEXT', }, ], @@ -3361,7 +3415,7 @@ function buildOptimisticModifiedExpenseReportAction( actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, actorAccountID: currentUserAccountID, automatic: false, - avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), created: DateUtils.getDBTime(), isAttachment: false, message: [ @@ -3444,7 +3498,7 @@ function buildOptimisticTaskReportAction(taskReportID: string, actionName: Origi actionName, actorAccountID: currentUserAccountID, automatic: false, - avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), isAttachment: false, originalMessage, message: [ @@ -3520,10 +3574,6 @@ function buildOptimisticChatReport( }; } -function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource { - return allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID); -} - /** * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically * @param [created] - Action created time @@ -3550,7 +3600,7 @@ function buildOptimisticCreatedReportAction(emailCreatingAction: string, created { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), }, ], automatic: false, @@ -3586,7 +3636,7 @@ function buildOptimisticRenamedRoomReportAction(newName: string, oldName: string { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), }, ], originalMessage: { @@ -3627,11 +3677,11 @@ function buildOptimisticHoldReportAction(comment: string, created = DateUtils.ge { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), }, ], automatic: false, - avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), created, shouldShow: true, }; @@ -3658,42 +3708,79 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), }, ], automatic: false, - avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: getCurrentUserAvatarOrDefault(), created, shouldShow: true, }; } -/** - * Returns the necessary reportAction onyx data to indicate that a task report has been edited - */ -function buildOptimisticEditedTaskReportAction(emailEditingTask: string): OptimisticEditedTaskReportAction { +function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): OptimisticEditedTaskReportAction { + // We do not modify title & description in one request, so we need to create a different optimistic action for each field modification + let field = ''; + let value = ''; + if (title !== undefined) { + field = 'task title'; + value = title; + } else if (description !== undefined) { + field = 'description'; + value = description; + } + + let changelog = 'edited this task'; + if (field && value) { + changelog = `updated the ${field} to ${value}`; + } else if (field) { + changelog = `removed the ${field}`; + } + return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, actorAccountID: currentUserAccountID, message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + text: changelog, + html: changelog, + }, + ], + person: [ { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: emailEditingTask, + text: getCurrentUserDisplayNameOrEmail(), }, + ], + automatic: false, + avatar: getCurrentUserAvatarOrDefault(), + created: DateUtils.getDBTime(), + shouldShow: false, + }; +} + +function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: number): OptimisticEditedTaskReportAction { + return { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + actorAccountID: currentUserAccountID, + message: [ { - type: CONST.REPORT.MESSAGE.TYPE.TEXT, - style: 'normal', - text: ' edited this task', + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + text: `assigned to ${getDisplayNameForParticipant(assigneeAccountID)}`, + html: `assigned to `, }, ], person: [ { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), }, ], automatic: false, @@ -3736,7 +3823,7 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + text: getCurrentUserDisplayNameOrEmail(), }, ], reportActionID: NumberUtils.rand64(), @@ -4001,7 +4088,7 @@ function shouldReportBeInOptionList({ report: OnyxEntry; currentReportId: string; isInGSDMode: boolean; - betas: Beta[]; + betas: OnyxEntry; policies: OnyxCollection; excludeEmptyChats: boolean; doesReportHaveViolations: boolean; @@ -4165,7 +4252,13 @@ function chatIncludesChronos(report: OnyxEntry | EmptyObject): boolean { * - It's an ADDCOMMENT that is not an attachment */ function canFlagReportAction(reportAction: OnyxEntry, reportID: string | undefined): boolean { - const report = getReport(reportID); + let report = getReport(reportID); + + // If the childReportID exists in reportAction and is equal to the reportID, + // the report action being evaluated is the parent report action in a thread, and we should get the parent report to evaluate instead. + if (reportAction?.childReportID?.toString() === reportID?.toString()) { + report = getReport(report?.parentReportID); + } const isCurrentUserAction = reportAction?.actorAccountID === currentUserAccountID; const isOriginalMessageHaveHtml = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || @@ -4340,7 +4433,6 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return false; } - // In case of expense reports, we have to look at the parent workspace chat to get the isOwnPolicyExpenseChat property let isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat ?? false; if (isExpenseReport(report) && getParentReport(report)) { isOwnPolicyExpenseChat = Boolean(getParentReport(report)?.isOwnPolicyExpenseChat); @@ -4354,12 +4446,8 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o // User can request money in any IOU report, unless paid, but user can only request money in an expense report // which is tied to their workspace chat. if (isMoneyRequestReport(report)) { - const isOwnExpenseReport = isExpenseReport(report) && isOwnPolicyExpenseChat; - if (isOwnExpenseReport && PolicyUtils.isPaidGroupPolicy(policy)) { - return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report); - } - - return (isOwnExpenseReport || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID); + const canAddTransactions = canAddOrDeleteTransactions(report); + return isGroupPolicy(report) ? isOwnPolicyExpenseChat && canAddTransactions : canAddTransactions; } // In case of policy expense chat, users can only request money from their own policy expense chat @@ -5119,6 +5207,17 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry | undefined | null, chatReport: OnyxEntry | null): boolean { + return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -5142,6 +5241,7 @@ export { getPolicyName, getPolicyType, isArchivedRoom, + isClosedExpenseReportWithNoExpenses, isExpensifyOnlyParticipantInReport, canCreateTaskInReport, isPolicyExpenseChatAdmin, @@ -5150,7 +5250,6 @@ export { isPublicAnnounceRoom, isConciergeChatReport, isProcessingReport, - isExpenseReportWithInstantSubmittedState, isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyAccountIDs, hasExpensifyGuidesEmails, @@ -5190,7 +5289,8 @@ export { buildOptimisticClosedReportAction, buildOptimisticCreatedReportAction, buildOptimisticRenamedRoomReportAction, - buildOptimisticEditedTaskReportAction, + buildOptimisticEditedTaskFieldReportAction, + buildOptimisticChangedTaskAssigneeReportAction, buildOptimisticIOUReport, buildOptimisticApprovedReportAction, buildOptimisticMovedReportAction, @@ -5324,6 +5424,9 @@ export { canEditRoomVisibility, canEditPolicyDescription, getPolicyDescriptionText, + isJoinRequestInAdminRoom, + canAddOrDeleteTransactions, + shouldCreateNewMoneyRequestReport, }; export type { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 8d53e992cb2d..3aa4cb63df9a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -328,7 +328,7 @@ function getOptionData({ const newName = lastAction?.originalMessage?.newName ?? ''; result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName}); } else if (ReportActionsUtils.isTaskAction(lastAction)) { - result.alternateText = TaskUtils.getTaskReportActionMessage(lastAction.actionName); + result.alternateText = TaskUtils.getTaskReportActionMessage(lastAction).text; } else if ( lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM || @@ -386,6 +386,12 @@ function getOptionData({ result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report); + if (ReportActionsUtils.isActionableJoinRequestPending(report.reportID)) { + result.isPinned = true; + result.isUnread = true; + result.brickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + } + if (!hasMultipleParticipants) { result.accountID = personalDetail?.accountID; result.login = personalDetail?.login; diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index 623d449db885..81a079003d0e 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -3,6 +3,7 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; +import type {Message} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import * as CollectionUtils from './CollectionUtils'; import * as Localize from './Localize'; @@ -22,16 +23,21 @@ Onyx.connect({ /** * Given the Task reportAction name, return the appropriate message to be displayed and copied to clipboard. */ -function getTaskReportActionMessage(actionName: string): string { - switch (actionName) { +function getTaskReportActionMessage(action: OnyxEntry): Pick { + switch (action?.actionName) { case CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED: - return Localize.translateLocal('task.messages.completed'); + return {text: Localize.translateLocal('task.messages.completed')}; case CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED: - return Localize.translateLocal('task.messages.canceled'); + return {text: Localize.translateLocal('task.messages.canceled')}; case CONST.REPORT.ACTIONS.TYPE.TASKREOPENED: - return Localize.translateLocal('task.messages.reopened'); + return {text: Localize.translateLocal('task.messages.reopened')}; + case CONST.REPORT.ACTIONS.TYPE.TASKEDITED: + return { + text: action?.message?.[0].text ?? '', + html: action?.message?.[0].html, + }; default: - return Localize.translateLocal('task.task'); + return {text: Localize.translateLocal('task.task')}; } } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5f9657755b02..cb3aa20ab6a7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -27,6 +27,7 @@ import type { import {WRITE_COMMANDS} from '@libs/API/types'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as IOUUtils from '@libs/IOUUtils'; @@ -222,12 +223,22 @@ Onyx.connect({ }, }); +let lastSelectedDistanceRates: OnyxEntry = {}; +Onyx.connect({ + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + callback: (value) => { + lastSelectedDistanceRates = value; + }, +}); + /** * Initialize money request info * @param reportID to attach the transaction to + * @param policy + * @param isFromGlobalCreate * @param iouRequestType one of manual/scan/distance */ -function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { +function initMoneyRequest(reportID: string, policy: OnyxEntry, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { // Generate a brand new transactionID const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID; // Disabling this line since currentDate can be an empty string @@ -241,6 +252,12 @@ function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequ waypoint0: {}, waypoint1: {}, }; + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + if (ReportUtils.isPolicyExpenseChat(report)) { + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; + } + comment.customUnit = {customUnitRateID}; } // Store the transaction in Onyx and mark it as not saved so it can be cleaned up later @@ -828,37 +845,26 @@ function getMoneyRequestInformation( // STEP 2: Get the money request report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report. // If no such reportID has been provided, let's use the chatReport.iouReportID property. In case that is not present, build a new optimistic money request report. let iouReport: OnyxEntry = null; - const shouldCreateNewMoneyRequestReport = !moneyRequestReportID && (!chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport)); if (moneyRequestReportID) { iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReportID}`] ?? null; - } else if (!shouldCreateNewMoneyRequestReport) { + } else { iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`] ?? null; } - let isFromPaidPolicy = false; - if (isPolicyExpenseChat) { - isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy ?? null); - - // If the linked expense report on paid policy is not draft and not instantly submitted, we need to create a new draft expense report - if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport) && !ReportUtils.isExpenseReportWithInstantSubmittedState(iouReport)) { - iouReport = null; - } - } + const shouldCreateNewMoneyRequestReport = ReportUtils.shouldCreateNewMoneyRequestReport(iouReport, chatReport); - if (iouReport) { - if (isPolicyExpenseChat) { - iouReport = {...iouReport}; - if (iouReport?.currency === currency && typeof iouReport.total === 'number') { - // Because of the Expense reports are stored as negative values, we subtract the total from the amount - iouReport.total -= amount; - } - } else { - iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency); - } - } else { + if (!iouReport || shouldCreateNewMoneyRequestReport) { iouReport = isPolicyExpenseChat ? ReportUtils.buildOptimisticExpenseReport(chatReport.reportID, chatReport.policyID ?? '', payeeAccountID, amount, currency) : ReportUtils.buildOptimisticIOUReport(payeeAccountID, payerAccountID, amount, chatReport.reportID, currency); + } else if (isPolicyExpenseChat) { + iouReport = {...iouReport}; + if (iouReport?.currency === currency && typeof iouReport.total === 'number') { + // Because of the Expense reports are stored as negative values, we subtract the total from the amount + iouReport.total -= amount; + } + } else { + iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency); } // STEP 3: Build optimistic receipt and transaction @@ -1843,10 +1849,8 @@ function createSplitsAndOnyxData( } // STEP 2: Get existing IOU/Expense report and update its total OR build a new optimistic one - // For Control policy expense chats, if the report is already approved, create a new expense report let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null; - const shouldCreateNewOneOnOneIOUReport = - !oneOnOneIOUReport || (isOwnPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport)); + const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport); if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) { oneOnOneIOUReport = isOwnPolicyExpenseChat @@ -2484,8 +2488,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA } let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport?.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null; - const shouldCreateNewOneOnOneIOUReport = - !oneOnOneIOUReport || (isPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport)); + const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport); if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) { oneOnOneIOUReport = isPolicyExpenseChat @@ -3638,6 +3641,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT chatReportID: chatReport.reportID, reportActionID: optimisticIOUReportAction.reportActionID, paymentMethodType, + amount: Math.abs(total), }, optimisticData, successData, diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index ab0dea960b27..b4554f9461ce 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -41,7 +41,8 @@ export default () => { if ( !(typeof value === 'object' && !!value) || !('type' in value) || - (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && value.updates)) + (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && + !((value.type === CONST.ONYX_UPDATE_TYPES.PUSHER || value.type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && value.updates)) ) { console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index cfb4735f0638..ab26ad330b6f 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {Merge} from 'type-fest'; import Log from '@libs/Log'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; import PusherUtils from '@libs/PusherUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -107,7 +108,7 @@ function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFrom if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) { return applyHTTPSOnyxUpdates(request, response); } - if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) { + if ((type === CONST.ONYX_UPDATE_TYPES.PUSHER || type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && updates) { return applyPusherOnyxUpdates(updates); } } @@ -141,5 +142,17 @@ function doesClientNeedToBeUpdated(previousUpdateID = 0): boolean { return lastUpdateIDAppliedToClient < previousUpdateID; } +function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer) { + const previousUpdateID = Number(updates.previousUpdateID) || 0; + if (!doesClientNeedToBeUpdated(previousUpdateID)) { + apply(updates); + return; + } + + // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. + SequentialQueue.pause(); + saveUpdateInformation(updates); +} + // eslint-disable-next-line import/prefer-default-export -export {saveUpdateInformation, doesClientNeedToBeUpdated, apply}; +export {saveUpdateInformation, doesClientNeedToBeUpdated, apply, applyOnyxUpdatesReliably}; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index aa64611b210f..f6a1ec3ec340 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -57,7 +57,8 @@ import type { ReportAction, Transaction, } from '@src/types/onyx'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; import type {Attributes, CustomUnit, Rate, Unit} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; @@ -749,9 +750,9 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, value: { - ...memberRoles.reduce((member: Record, current) => { + ...memberRoles.reduce((member: Record, current) => { // eslint-disable-next-line no-param-reassign - member[current.accountID] = {role: current?.role}; + member[current.accountID] = {role: current?.role, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}; return member; }, {}), errors: null, @@ -764,6 +765,11 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, value: { + ...memberRoles.reduce((member: Record, current) => { + // eslint-disable-next-line no-param-reassign + member[current.accountID] = {role: current?.role, pendingAction: null}; + return member; + }, {}), errors: null, }, }, @@ -964,6 +970,37 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount API.write(WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE, params, {optimisticData, successData, failureData}); } +/** + * Invite member to the specified policyID + * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details + */ +function inviteMemberToWorkspace(policyID: string, inviterEmail: string) { + const memberJoinKey = `${ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER}${policyID}` as const; + + const optimisticMembersState = {policyID, inviterEmail}; + const failureMembersState = {policyID, inviterEmail}; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: memberJoinKey, + value: optimisticMembersState, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: memberJoinKey, + value: {...failureMembersState, errors: ErrorUtils.getMicroSecondOnyxError('common.genericEditFailureMessage')}, + }, + ]; + + const params = {policyID, inviterEmail}; + + API.write(WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK, params, {optimisticData, failureData}); +} + /** * Updates a workspace avatar image */ @@ -2435,6 +2472,56 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor API.write('SetWorkspaceCategoriesEnabled', parameters, onyxData); } +function createPolicyCategory(policyID: string, categoryName: string) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + name: categoryName, + enabled: true, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + errors: null, + pendingAction: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'), + pendingAction: null, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + categories: JSON.stringify([{name: categoryName}]), + }; + + API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData); +} + function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) { const onyxData: OnyxData = { optimisticData: [ @@ -2503,6 +2590,123 @@ function clearCategoryErrors(policyID: string, categoryName: string) { }); } +/** + * Accept user join request to a workspace + */ +function acceptJoinRequest(reportID: string, reportAction: OnyxEntry) { + const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT; + if (!reportAction) { + return; + } + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportAction.reportActionID]: { + originalMessage: {choice}, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportAction.reportActionID]: { + originalMessage: {choice}, + pendingAction: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportAction.reportActionID]: { + originalMessage: {choice: ''}, + pendingAction: null, + }, + }, + }, + ]; + + const parameters = { + requests: JSON.stringify({ + [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: { + requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}], + }, + }), + }; + + API.write(WRITE_COMMANDS.ACCEPT_JOIN_REQUEST, parameters, {optimisticData, failureData, successData}); +} + +/** + * Decline user join request to a workspace + */ +function declineJoinRequest(reportID: string, reportAction: OnyxEntry) { + if (!reportAction) { + return; + } + const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE; + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportAction.reportActionID]: { + originalMessage: {choice}, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportAction.reportActionID]: { + originalMessage: {choice}, + pendingAction: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportAction.reportActionID]: { + originalMessage: {choice: ''}, + pendingAction: null, + }, + }, + }, + ]; + + const parameters = { + requests: JSON.stringify({ + [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: { + requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}], + }, + }), + }; + + API.write(WRITE_COMMANDS.DECLINE_JOIN_REQUEST, parameters, {optimisticData, failureData, successData}); +} + export { removeMembers, updateWorkspaceMembersRole, @@ -2552,5 +2756,9 @@ export { updateWorkspaceDescription, setWorkspaceCategoryEnabled, setWorkspaceRequiresCategory, + inviteMemberToWorkspace, + acceptJoinRequest, + declineJoinRequest, + createPolicyCategory, clearCategoryErrors, }; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7ad12cf3e1ed..94fe324d306a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -113,15 +113,31 @@ Onyx.connect({ }, }); +// map of reportID to all reportActions for that report const allReportActions: OnyxCollection = {}; + +// map of reportID to the ID of the oldest reportAction for that report +const oldestReportActions: Record = {}; + +// map of report to the ID of the newest action for that report +const newestReportActions: Record = {}; + Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (action, key) => { - if (!key || !action) { + callback: (actions, key) => { + if (!key || !actions) { return; } const reportID = CollectionUtils.extractCollectionItemID(key); - allReportActions[reportID] = action; + allReportActions[reportID] = actions; + const sortedActions = ReportActionsUtils.getSortedReportActions(Object.values(actions)); + + if (sortedActions.length === 0) { + return; + } + + oldestReportActions[reportID] = sortedActions[0].reportActionID; + newestReportActions[reportID] = sortedActions[sortedActions.length - 1].reportActionID; }, }); @@ -879,7 +895,7 @@ function reconnect(reportID: string) { * Gets the older actions that have not been read yet. * Normally happens when you scroll up on a chat, and the actions have not been read yet. */ -function getOlderActions(reportID: string, reportActionID: string) { +function getOlderActions(reportID: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -912,7 +928,7 @@ function getOlderActions(reportID: string, reportActionID: string) { const parameters: GetOlderActionsParams = { reportID, - reportActionID, + reportActionID: oldestReportActions[reportID], }; API.read(READ_COMMANDS.GET_OLDER_ACTIONS, parameters, {optimisticData, successData, failureData}); @@ -922,7 +938,7 @@ function getOlderActions(reportID: string, reportActionID: string) { * Gets the newer actions that have not been read yet. * Normally happens when you are not located at the bottom of the list and scroll down on a chat. */ -function getNewerActions(reportID: string, reportActionID: string) { +function getNewerActions(reportID: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -955,7 +971,7 @@ function getNewerActions(reportID: string, reportActionID: string) { const parameters: GetNewerActionsParams = { reportID, - reportActionID, + reportActionID: newestReportActions[reportID], }; API.read(READ_COMMANDS.GET_NEWER_ACTIONS, parameters, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index e328460c37eb..27c7f3e36fd4 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -399,7 +399,7 @@ function reopenTask(taskReport: OnyxEntry) { function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task) { // Create the EditedReportAction on the task - const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail); + const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskFieldReportAction({title, description}); // Sometimes title or description is undefined, so we need to check for that, and we provide it to multiple functions const reportName = (title ?? report?.reportName)?.trim(); @@ -429,6 +429,11 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task ]; const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: {[editTaskReportAction.reportActionID]: {pendingAction: null}}, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, @@ -467,16 +472,22 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task API.write(WRITE_COMMANDS.EDIT_TASK, parameters, {optimisticData, successData, failureData}); } -function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assigneeEmail: string, assigneeAccountID = 0, assigneeChatReport: OnyxEntry = null) { +function editTaskAssignee( + report: OnyxTypes.Report, + ownerAccountID: number, + assigneeEmail: string, + assigneeAccountID: number | null = 0, + assigneeChatReport: OnyxEntry = null, +) { // Create the EditedReportAction on the task - const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail); + const editTaskReportAction = ReportUtils.buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID ?? 0); const reportName = report.reportName?.trim(); let assigneeChatReportOnyxData; const assigneeChatReportID = assigneeChatReport ? assigneeChatReport.reportID : '0'; const optimisticReport: OptimisticReport = { reportName, - managerID: assigneeAccountID || report.managerID, + managerID: assigneeAccountID ?? report.managerID, pendingFields: { ...(assigneeAccountID && {managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), }, @@ -499,6 +510,11 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi ]; const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: {[editTaskReportAction.reportActionID]: {pendingAction: null}}, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 7b146f7447bb..fdd657f801f2 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -578,35 +578,15 @@ function subscribeToUserEvents() { // Handles the mega multipleEvents from Pusher which contains an array of single events. // Each single event is passed to PusherUtils in order to trigger the callbacks for that event PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON) => { - // The data for this push event comes in two different formats: - // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete - // - The data is an array of objects, where each object is an onyx update - // Example: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}] - // 1. Reliable updates format - this is what was sent with the RELIABLE_UPDATES project and will be the format from now on - // - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above) - // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]} - if (Array.isArray(pushJSON)) { - Log.warn('Received pusher event with array format'); - pushJSON.forEach((multipleEvent) => { - PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data); - }); - return; - } - + // The data for the update is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above) + // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]} const updates = { type: CONST.ONYX_UPDATE_TYPES.PUSHER, lastUpdateID: Number(pushJSON.lastUpdateID || 0), updates: pushJSON.updates ?? [], previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; - if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { - OnyxUpdates.apply(updates); - return; - } - - // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. - SequentialQueue.pause(); - OnyxUpdates.saveUpdateInformation(updates); + OnyxUpdates.applyOnyxUpdatesReliably(updates); }); // Handles Onyx updates coming from Pusher through the mega multipleEvents. diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 3dc5924d023a..9fe6e8f018d8 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-imports */ -import type {Text as RNText, View} from 'react-native'; import type {ValueOf} from 'type-fest'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; @@ -13,9 +13,9 @@ type AnchorOrigin = { /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. */ -export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise { +export default function calculateAnchorPosition(anchorComponent: ContextMenuAnchor, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { - if (!anchorComponent) { + if (!anchorComponent || !('measureInWindow' in anchorComponent)) { resolve({horizontal: 0, vertical: 0}); return; } diff --git a/src/libs/focusTextInputAfterAnimation/index.android.ts b/src/libs/focusTextInputAfterAnimation/index.android.ts index 31c748f5daa4..cca8a6588103 100644 --- a/src/libs/focusTextInputAfterAnimation/index.android.ts +++ b/src/libs/focusTextInputAfterAnimation/index.android.ts @@ -19,7 +19,7 @@ import type FocusTextInputAfterAnimation from './types'; */ const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef, animationLength = 0) => { setTimeout(() => { - inputRef.focus(); + inputRef?.focus(); }, animationLength); }; diff --git a/src/libs/focusTextInputAfterAnimation/index.ts b/src/libs/focusTextInputAfterAnimation/index.ts index 3f7c6555b5ce..66d0c35c1a63 100644 --- a/src/libs/focusTextInputAfterAnimation/index.ts +++ b/src/libs/focusTextInputAfterAnimation/index.ts @@ -4,7 +4,7 @@ import type FocusTextInputAfterAnimation from './types'; * This library is a no-op for all platforms except for Android and iOS and will immediately focus the given input without any delays. */ const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef) => { - inputRef.focus(); + inputRef?.focus(); }; export default focusTextInputAfterAnimation; diff --git a/src/libs/focusTextInputAfterAnimation/types.ts b/src/libs/focusTextInputAfterAnimation/types.ts index a6a14165598b..bfe29317c1ef 100644 --- a/src/libs/focusTextInputAfterAnimation/types.ts +++ b/src/libs/focusTextInputAfterAnimation/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement, animationLength: number) => void; +type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement | undefined, animationLength: number) => void; export default FocusTextInputAfterAnimation; diff --git a/src/libs/getClickedTargetLocation/types.ts b/src/libs/getClickedTargetLocation/types.ts index 7b1e85e63b17..eed10238be2d 100644 --- a/src/libs/getClickedTargetLocation/types.ts +++ b/src/libs/getClickedTargetLocation/types.ts @@ -1,5 +1,5 @@ type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | 'y'; -type GetClickedTargetLocation = (target: Element) => Pick; +type GetClickedTargetLocation = (target: HTMLDivElement) => Pick; export default GetClickedTargetLocation; diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index fd03adcffd93..330ba4470097 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -8,15 +8,15 @@ import type {Message} from '@src/types/onyx/ReportAction'; * * @param reportActionMessage report action's message as text, html and translationKey */ -export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean { - if (!text || !html) { +export default function isReportMessageAttachment(message: Message | undefined): boolean { + if (!message?.text || !message.html) { return false; } - if (translationKey && text === CONST.ATTACHMENT_MESSAGE_TEXT) { - return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; + if (message.translationKey && message.text === CONST.ATTACHMENT_MESSAGE_TEXT) { + return message?.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT; } const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i'); - return (text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(text)) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); + return (message.text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(message.text)) && (!!message.html.match(regex) || message.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML); } diff --git a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts index dbf2829a6c28..c8ef72ca15e7 100644 --- a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts +++ b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts @@ -47,6 +47,7 @@ export default function () { // If newReportActionsDrafts[newOnyxKey] isn't set, fall back on the migrated draft if there is one const currentActionsDrafts = newReportActionsDrafts[newOnyxKey] ?? allReportActionsDrafts[newOnyxKey]; + newReportActionsDrafts[newOnyxKey] = { ...currentActionsDrafts, [reportActionID]: reportActionDraft, diff --git a/src/libs/navigateAfterJoinRequest/index.desktop.ts b/src/libs/navigateAfterJoinRequest/index.desktop.ts new file mode 100644 index 000000000000..47180c6a1368 --- /dev/null +++ b/src/libs/navigateAfterJoinRequest/index.desktop.ts @@ -0,0 +1,8 @@ +import Navigation from '@navigation/Navigation'; +import ROUTES from '@src/ROUTES'; + +const navigateAfterJoinRequest = () => { + Navigation.goBack(undefined, false, true); + Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); +}; +export default navigateAfterJoinRequest; diff --git a/src/libs/navigateAfterJoinRequest/index.ts b/src/libs/navigateAfterJoinRequest/index.ts new file mode 100644 index 000000000000..b9e533208ec2 --- /dev/null +++ b/src/libs/navigateAfterJoinRequest/index.ts @@ -0,0 +1,8 @@ +import Navigation from '@navigation/Navigation'; +import ROUTES from '@src/ROUTES'; + +const navigateAfterJoinRequest = () => { + Navigation.goBack(undefined, false, true); + Navigation.navigate(ROUTES.ALL_SETTINGS); +}; +export default navigateAfterJoinRequest; diff --git a/src/libs/navigateAfterJoinRequest/index.web.ts b/src/libs/navigateAfterJoinRequest/index.web.ts new file mode 100644 index 000000000000..47180c6a1368 --- /dev/null +++ b/src/libs/navigateAfterJoinRequest/index.web.ts @@ -0,0 +1,8 @@ +import Navigation from '@navigation/Navigation'; +import ROUTES from '@src/ROUTES'; + +const navigateAfterJoinRequest = () => { + Navigation.goBack(undefined, false, true); + Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); +}; +export default navigateAfterJoinRequest; diff --git a/src/pages/DetailsPage.tsx b/src/pages/DetailsPage.tsx index a9adb5310e58..b3b0f0782ba0 100755 --- a/src/pages/DetailsPage.tsx +++ b/src/pages/DetailsPage.tsx @@ -1,7 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; @@ -15,6 +15,7 @@ import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/EnablePayments/TermsStep.js b/src/pages/EnablePayments/TermsStep.js index 9fa3a4becea3..a55816d207be 100644 --- a/src/pages/EnablePayments/TermsStep.js +++ b/src/pages/EnablePayments/TermsStep.js @@ -1,9 +1,9 @@ import React, {useEffect, useState} from 'react'; -import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; diff --git a/src/pages/FlagCommentPage.tsx b/src/pages/FlagCommentPage.tsx index 00c38dabc4ec..216196c17d55 100644 --- a/src/pages/FlagCommentPage.tsx +++ b/src/pages/FlagCommentPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; @@ -9,6 +9,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/GetAssistancePage.tsx b/src/pages/GetAssistancePage.tsx index 948e0c239de9..b543524fc68e 100644 --- a/src/pages/GetAssistancePage.tsx +++ b/src/pages/GetAssistancePage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import type {MenuItemWithLink} from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/KeyboardShortcutsPage.tsx b/src/pages/KeyboardShortcutsPage.tsx index 9b70defbf8af..d68643e74a5a 100644 --- a/src/pages/KeyboardShortcutsPage.tsx +++ b/src/pages/KeyboardShortcutsPage.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx index f27c821abd8c..559da335cf13 100644 --- a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx +++ b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -7,6 +7,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import type {MenuItemProps} from '@components/MenuItem'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx index 747b23e943ca..3c7520b850b4 100644 --- a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx +++ b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -8,6 +8,7 @@ import LottieAnimations from '@components/LottieAnimations'; import type {MenuItemProps} from '@components/MenuItem'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 0a6a2659ffb6..1893f81da2fe 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -1,11 +1,11 @@ import React, {useMemo} from 'react'; -import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index a4c740250908..cc533dbc3a08 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import AutoUpdateTime from '@components/AutoUpdateTime'; @@ -17,6 +17,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 75ae02587486..e18155ea6139 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -11,6 +11,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; diff --git a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx index b128d6dc75e8..af4b251952de 100644 --- a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx @@ -1,10 +1,11 @@ import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx index 2b742ad65699..4228b1da9d12 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx index 25ce2d7b81da..42bf43d78910 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx index 6a94a7b456f3..a5b839118edc 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx @@ -1,6 +1,5 @@ import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import React, {useMemo} from 'react'; -import {ScrollView} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; @@ -8,6 +7,7 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx b/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx index 2bf76d714cf5..65f7f14d6c91 100644 --- a/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx +++ b/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import {ScrollView} from 'react-native'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js index d1ac0989ae38..9c28fe928d33 100644 --- a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js +++ b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js @@ -1,7 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; -import {ScrollView} from 'react-native'; import _ from 'underscore'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -10,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; diff --git a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx index fd2f05493098..4c4bd9a20b71 100644 --- a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx +++ b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import {ScrollView} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -10,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx index b4272f094071..f05bb70bcd5a 100644 --- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx @@ -1,10 +1,11 @@ import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js index 8cca56779059..fac405090de7 100644 --- a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js +++ b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js @@ -1,12 +1,12 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; -import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Onfido from '@components/Onfido'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Growl from '@libs/Growl'; diff --git a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx index d17166365a39..cb9763b5cc25 100644 --- a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx +++ b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx @@ -1,5 +1,5 @@ import React, {useCallback} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -8,6 +8,7 @@ import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; // @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript. import Onfido from '@components/Onfido'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Growl from '@libs/Growl'; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e94c0cc80952..d96e01c1a4d3 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,7 +1,7 @@ import {useRoute} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -17,6 +17,7 @@ import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx index f2bba4b17a9a..4f1bac01b556 100644 --- a/src/pages/ShareCodePage.tsx +++ b/src/pages/ShareCodePage.tsx @@ -1,5 +1,5 @@ import React, {useMemo, useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {ImageSourcePropType} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; @@ -10,6 +10,7 @@ import MenuItem from '@components/MenuItem'; import QRShareWithDownload from '@components/QRShare/QRShareWithDownload'; import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts index e9440ab932d6..6f177098c2c4 100644 --- a/src/pages/home/ReportScreenContext.ts +++ b/src/pages/home/ReportScreenContext.ts @@ -1,8 +1,9 @@ import type {RefObject, SyntheticEvent} from 'react'; import {createContext} from 'react'; -import type {FlatList, GestureResponderEvent, View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {FlatList, GestureResponderEvent, Text, View} from 'react-native'; -type ReactionListAnchor = View | HTMLDivElement | null; +type ReactionListAnchor = View | Text | HTMLDivElement | null; type ReactionListEvent = GestureResponderEvent | MouseEvent | SyntheticEvent; diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 4f6e0548eb72..974a8824f5ff 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -22,7 +22,7 @@ import type {Beta, ReportAction, ReportActions} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions'; import ContextMenuActions from './ContextMenuActions'; -import type {ContextMenuType} from './ReportActionContextMenu'; +import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { @@ -64,7 +64,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { type?: ContextMenuType; /** Target node which is the target of ContentMenu */ - anchor?: MutableRefObject; + anchor?: MutableRefObject; /** Flag to check if the chat participant is Chronos */ isChronosReport?: boolean; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 831b32def2bb..ffdbcab577b7 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,7 +1,8 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import type {MutableRefObject} from 'react'; import React from 'react'; -import type {GestureResponderEvent} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, Text, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -13,6 +14,7 @@ import EmailUtils from '@libs/EmailUtils'; import * as Environment from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import * as Localize from '@libs/Localize'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; @@ -28,6 +30,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type {Beta, ReportAction, ReportActionReactions, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; +import type {ContextMenuAnchor} from './ReportActionContextMenu'; import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; /** Gets the HTML version of the message in an action */ @@ -52,7 +55,7 @@ type ShouldShow = ( reportAction: OnyxEntry, isArchivedRoom: boolean, betas: OnyxEntry, - menuTarget: MutableRefObject | undefined, + menuTarget: MutableRefObject | undefined, isChronosReport: boolean, reportID: string, isPinnedChat: boolean, @@ -69,6 +72,8 @@ type ContextMenuActionPayload = { close: () => void; openContextMenu: () => void; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + anchor?: MutableRefObject; + checkIfContextMenuActive?: () => void; openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; setIsEmojiPickerActive?: (state: boolean) => void; @@ -342,9 +347,8 @@ const ContextMenuActions: ContextMenuAction[] = [ // `ContextMenuItem` with `successText` and `successIcon` which will fall back to // the `text` and `icon` onPress: (closePopover, {reportAction, selection, reportID}) => { - const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); - const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionHtml(reportAction); + const messageHtml = getActionHtml(reportAction); const messageText = ReportActionsUtils.getReportActionMessageText(reportAction); const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); @@ -354,6 +358,9 @@ const ContextMenuActions: ContextMenuAction[] = [ const iouReport = ReportUtils.getReport(ReportActionsUtils.getIOUReportIDFromReportActionPreview(reportAction)); const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction); Clipboard.setString(displayMessage); + } else if (ReportActionsUtils.isTaskAction(reportAction)) { + const displayMessage = TaskUtils.getTaskReportActionMessage(reportAction).text; + Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportID, reportAction); Clipboard.setString(modifyExpenseMessage); @@ -376,6 +383,10 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (ReportActionsUtils.isActionableMentionWhisper(reportAction)) { const mentionWhisperMessage = ReportActionsUtils.getActionableMentionWhisperMessage(reportAction); setClipboardMessage(mentionWhisperMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { + Clipboard.setString(Localize.translateLocal('iou.heldRequest', {comment: reportAction.message?.[1]?.text ?? ''})); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { + Clipboard.setString(Localize.translateLocal('iou.unheldRequest')); } else if (content) { setClipboardMessage(content); } else if (messageText) { @@ -399,7 +410,7 @@ const ContextMenuActions: ContextMenuAction[] = [ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); // Only hide the copylink menu item when context menu is opened over img element. - const isAttachmentTarget = menuTarget?.current?.tagName === 'IMG' && isAttachment; + const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction); }, onPress: (closePopover, {reportAction, reportID}) => { diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts index 98b38dcb6968..b7c3d6214094 100644 --- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts +++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts @@ -1,6 +1,6 @@ import type {BaseReportActionContextMenuProps} from '@pages/home/report/ContextMenu/BaseReportActionContextMenu'; -type MiniReportActionContextMenuProps = Omit & { +type MiniReportActionContextMenuProps = Omit & { /** Should the reportAction this menu is attached to have the appearance of being grouped with the previous reportAction? */ displayAsGroup?: boolean; }; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 862d5f01c2fc..931b87704ce5 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -67,8 +67,8 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef(null); const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); - const contextMenuAnchorRef = useRef(null); - const contextMenuTargetNode = useRef(null); + const contextMenuAnchorRef = useRef(null); + const contextMenuTargetNode = useRef(null); const onPopoverShow = useRef(() => {}); const onPopoverHide = useRef(() => {}); @@ -83,7 +83,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef new Promise((resolve) => { - if (contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { + if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y})); } else { resolve({x: 0, y: 0}); @@ -169,7 +169,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; - contextMenuTargetNode.current = event.target as HTMLElement; + contextMenuTargetNode.current = event.target as HTMLDivElement; if (shouldCloseOnTarget) { anchorRef.current = event.target as HTMLDivElement; } else { diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index f2537c56a5af..21c1eea18e03 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -16,7 +16,7 @@ type OnCancel = () => void; type ContextMenuType = ValueOf; -type ContextMenuAnchor = View | RNText | null | undefined; +type ContextMenuAnchor = View | RNText | HTMLDivElement | null | undefined; type ShowContextMenu = ( type: ContextMenuType, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.tsx similarity index 51% rename from src/pages/home/report/ReportActionItem.js rename to src/pages/home/report/ReportActionItem.tsx index fb4a1f52b51a..744f0afb857b 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,9 +1,11 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import lodashIsEqual from 'lodash/isEqual'; +import lodashIsEmpty from 'lodash/isEmpty'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, TextInput} from 'react-native'; import {InteractionManager, View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {Emoji} from '@assets/emojis/types'; import Button from '@components/Button'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; @@ -12,11 +14,11 @@ import * as Expensicons from '@components/Icon/Expensicons'; import InlineSystemMessage from '@components/InlineSystemMessage'; import KYCWall from '@components/KYCWall'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider'; +import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; -import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes'; import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; import RenderHTML from '@components/RenderHTML'; +import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons'; import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions'; import MoneyReportView from '@components/ReportActionItem/MoneyReportView'; @@ -30,14 +32,13 @@ import TaskView from '@components/ReportActionItem/TaskView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import UnreadActionIndicator from '@components/UnreadActionIndicator'; -import withLocalize from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import ControlSelection from '@libs/ControlSelection'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -49,11 +50,10 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SelectionScraper from '@libs/SelectionScraper'; -import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; -import reportPropTypes from '@pages/reportPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; +import * as Policy from '@userActions/Policy'; import * as store from '@userActions/ReimbursementAccount/store'; import * as Report from '@userActions/Report'; import * as ReportActions from '@userActions/ReportActions'; @@ -62,6 +62,9 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {OriginalMessageActionableMentionWhisper, OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; @@ -75,233 +78,258 @@ import ReportActionItemMessage from './ReportActionItemMessage'; import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; -import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -import transactionPropTypes from '@components/transactionPropTypes'; -const propTypes = { - ...windowDimensionsPropTypes, +const getDraftMessage = (drafts: OnyxCollection, reportID: string, action: OnyxTypes.ReportAction): string | undefined => { + const originalReportID = ReportUtils.getOriginalReportID(reportID, action); + const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; + const draftMessage = drafts?.[draftKey]?.[action.reportActionID]; + return typeof draftMessage === 'string' ? draftMessage : draftMessage?.message; +}; - /** Report for this action */ - report: reportPropTypes.isRequired, +type ReportActionItemOnyxProps = { + /** Stores user's preferred skin tone */ + preferredSkinTone: OnyxEntry; - /** All the data of the action item */ - action: PropTypes.shape(reportActionPropTypes).isRequired, + /** All reports shared with the user */ + reports: OnyxCollection; - /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: PropTypes.bool.isRequired, + /** IOU report for this action, if any */ + iouReport: OnyxEntry; - /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, + emojiReactions: OnyxEntry; - /** Should we display the new marker on top of the comment? */ - shouldDisplayNewMarker: PropTypes.bool.isRequired, + /** The user's wallet account */ + userWallet: OnyxEntry; - /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ - shouldShowSubscriptAvatar: PropTypes.bool, + /** All the report actions belonging to the report's parent */ + parentReportActions: OnyxEntry; - /** Position index of the report action in the overall report FlatList view */ - index: PropTypes.number.isRequired, + /** All policy report fields */ + policyReportFields: OnyxEntry; - /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage: PropTypes.string, + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; - /** Stores user's preferred skin tone */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** Array of report actions for this report */ + reportActions: OnyxEntry; - emojiReactions: EmojiReactionsPropTypes, + /** All the transactions shared with the user */ + transactions: OnyxCollection; +}; - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), +type ReportActionItemProps = { + /** Report for this action */ + report: OnyxTypes.Report; - /** IOU report for this action, if any */ - iouReport: reportPropTypes, + /** All the data of the action item */ + action: OnyxTypes.ReportAction; - /** Flag to show, hide the thread divider line */ - shouldHideThreadDividerLine: PropTypes.bool, + /** Should the comment have the appearance of being grouped with the previous comment? */ + displayAsGroup: boolean; - /** The user's wallet account */ - userWallet: userWalletPropTypes, + /** Is this the most recent IOU Action? */ + isMostRecentIOUReportAction: boolean; - /** All the report actions belonging to the report's parent */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** Should we display the new marker on top of the comment? */ + shouldDisplayNewMarker: boolean; - /** All the report actions belonging to the current report */ - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ + shouldShowSubscriptAvatar?: boolean; - /** All the transactions shared wit hthe user */ - transactions: PropTypes.objectOf(PropTypes.shape(transactionPropTypes)), + /** Position index of the report action in the overall report FlatList view */ + index: number; - /** Callback to be called on onPress */ - onPress: PropTypes.func, -}; + /** Flag to show, hide the thread divider line */ + shouldHideThreadDividerLine?: boolean; -const defaultProps = { - draftMessage: undefined, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - emojiReactions: {}, - shouldShowSubscriptAvatar: false, - reports: {}, - iouReport: undefined, - shouldHideThreadDividerLine: false, - userWallet: {}, - parentReportActions: {}, - reportActions: {}, - transactions: {}, - onPress: undefined, -}; + linkedReportActionID?: string; -function ReportActionItem(props) { + /** Callback to be called on onPress */ + onPress?: () => void; +} & ReportActionItemOnyxProps; + +const isIOUReport = (actionObj: OnyxEntry): actionObj is OnyxTypes.ReportActionBase & OnyxTypes.OriginalMessageIOU => + actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; + +function ReportActionItem({ + action, + report, + reports, + linkedReportActionID, + displayAsGroup, + emojiReactions, + index, + iouReport, + isMostRecentIOUReportAction, + parentReportActions, + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + shouldDisplayNewMarker, + userWallet, + shouldHideThreadDividerLine = false, + shouldShowSubscriptAvatar = false, + policyReportFields, + policy, + reportActions, + transactions, + onPress = undefined, +}: ReportActionItemProps) { + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const blockedFromConcierge = useBlockedFromConcierge(); + const reportActionDrafts = useReportActionsDrafts(); + const draftMessage = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); - const [isEmojiPickerActive, setIsEmojiPickerActive] = useState(); + const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID)); + const [isEmojiPickerActive, setIsEmojiPickerActive] = useState(); const [isHidden, setIsHidden] = useState(false); - const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); + const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); - const textInputRef = useRef(); - const popoverAnchorRef = useRef(); - const downloadedPreviews = useRef([]); - const prevDraftMessage = usePrevious(props.draftMessage); - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); - const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID); - const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID; - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(props.reportActions); - let transaction = {}; + const textInputRef = useRef(); + const popoverAnchorRef = useRef(null); + const downloadedPreviews = useRef([]); + const prevDraftMessage = usePrevious(draftMessage); + const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action); + const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID); + const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID; + const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportActions); const transactionThreadReport = useMemo(() => { - if (transactionThreadReportID === '0') { - return {}; - } - const report = props.reports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? {}; + return reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]; + }, [reports, transactionThreadReportID]); - // Get the transaction associated with the report - const transactionID = props.reportActions?.[report.parentReportActionID ?? '']?.originalMessage?.IOUTransactionID ?? 0; - transaction = props.transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - return report; - }, [props.reports, transactionThreadReportID, props.reportActions, props.transactions]); + // Get the transaction associated with the report + const transaction = useMemo(() => { + const reportAction = reportActions?.[transactionThreadReport?.parentReportActionID ?? '']; + const transactionID = reportAction?.originalMessage?.IOUReportID ?? 0; + return transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + }, [transactionThreadReport, reportActions, transactions]); - const transactionCurrency = !_.isEmpty(transaction) ? (transaction.modifiedCurrency ?? transaction.currency) : props.report.currency; + const transactionCurrency = !lodashIsEmpty(transaction) ? (transaction?.modifiedCurrency ?? transaction?.currency) : report.currency; const reportScrollManager = useReportScrollManager(); const highlightedBackgroundColorIfNeeded = useMemo( () => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}), [StyleUtils, isReportActionLinked, theme.hoverComponentBG], ); - const originalMessage = lodashGet(props.action, 'originalMessage', {}); - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(props.action); - const prevActionResolution = usePrevious(lodashGet(props.action, 'originalMessage.resolution', null)); + + const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); + const prevActionResolution = usePrevious(ReportActionsUtils.isActionableMentionWhisper(action) ? action.originalMessage.resolution : null); // IOUDetails only exists when we are sending money - const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails'); + const isSendingMoney = isIOUReport(action) && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails; const updateHiddenState = useCallback( - (isHiddenValue) => { + (isHiddenValue: boolean) => { setIsHidden(isHiddenValue); - const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message)); + const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.at(-1)); if (!isAttachment) { return; } - updateHiddenAttachments(props.action.reportActionID, isHiddenValue); + updateHiddenAttachments(action.reportActionID, isHiddenValue); }, - [props.action.reportActionID, props.action.message, updateHiddenAttachments], + [action.reportActionID, action.message, updateHiddenAttachments], ); useEffect( () => () => { // ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components, // we should also hide them when the current component is destroyed - if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) { + if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { ReportActionContextMenu.hideContextMenu(); ReportActionContextMenu.hideDeleteModal(); } - if (EmojiPickerAction.isActive(props.action.reportActionID)) { + if (EmojiPickerAction.isActive(action.reportActionID)) { EmojiPickerAction.hideEmojiPicker(true); } - if (reactionListRef.current && reactionListRef.current.isActiveReportAction(props.action.reportActionID)) { - reactionListRef.current.hideReactionList(); + if (reactionListRef?.current?.isActiveReportAction(action.reportActionID)) { + reactionListRef?.current?.hideReactionList(); } }, - [props.action.reportActionID, reactionListRef], + [action.reportActionID, reactionListRef], ); useEffect(() => { // We need to hide EmojiPicker when this is a deleted parent action - if (!isDeletedParentAction || !EmojiPickerAction.isActive(props.action.reportActionID)) { + if (!isDeletedParentAction || !EmojiPickerAction.isActive(action.reportActionID)) { return; } EmojiPickerAction.hideEmojiPicker(true); - }, [isDeletedParentAction, props.action.reportActionID]); + }, [isDeletedParentAction, action.reportActionID]); useEffect(() => { - if (!_.isUndefined(prevDraftMessage) || _.isUndefined(props.draftMessage)) { + if (prevDraftMessage !== undefined || draftMessage === undefined) { return; } focusTextInputAfterAnimation(textInputRef.current, 100); - }, [prevDraftMessage, props.draftMessage]); + }, [prevDraftMessage, draftMessage]); useEffect(() => { if (!Permissions.canUseLinkPreviews()) { return; } - const urls = ReportActionsUtils.extractLinksFromMessageHtml(props.action); - if (_.isEqual(downloadedPreviews.current, urls) || props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + const urls = ReportActionsUtils.extractLinksFromMessageHtml(action); + if (lodashIsEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } downloadedPreviews.current = urls; - Report.expandURLPreview(props.report.reportID, props.action.reportActionID); - }, [props.action, props.report.reportID]); + Report.expandURLPreview(report.reportID, action.reportActionID); + }, [action, report.reportID]); useEffect(() => { - if (_.isUndefined(props.draftMessage) || !ReportActionsUtils.isDeletedAction(props.action)) { + if (draftMessage === undefined || !ReportActionsUtils.isDeletedAction(action)) { return; } - Report.deleteReportActionDraft(props.report.reportID, props.action); - }, [props.draftMessage, props.action, props.report.reportID]); + Report.deleteReportActionDraft(report.reportID, action); + }, [draftMessage, action, report.reportID]); // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator // Removed messages should not be shown anyway and should not need this flow - const latestDecision = lodashGet(props, ['action', 'message', 0, 'moderationDecision', 'decision'], ''); + const latestDecision = action.message?.[0].moderationDecision?.decision ?? ''; useEffect(() => { - if (props.action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { + if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) { return; } // Hide reveal message button and show the message if latestDecision is changed to empty - if (_.isEmpty(latestDecision)) { + if (!latestDecision) { setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED); setIsHidden(false); return; } setModerationDecision(latestDecision); - if (!_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], latestDecision) && !ReportActionsUtils.isPendingRemove(props.action)) { + if ( + ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision) && + !ReportActionsUtils.isPendingRemove(action) + ) { setIsHidden(true); return; } setIsHidden(false); - }, [latestDecision, props.action]); + }, [latestDecision, action]); const toggleContextMenuFromActiveReportAction = useCallback(() => { - setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); - }, [props.action.reportActionID]); + setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(action.reportActionID)); + }, [action.reportActionID]); /** * Show the ReportActionContextMenu modal popover. * - * @param {Object} [event] - A press event. + * @param [event] - A press event. */ const showPopover = useCallback( - (event) => { + (event: GestureResponderEvent | MouseEvent) => { // Block menu on the message being Edited or if the report action item has errors - if (!_.isUndefined(props.draftMessage) || !_.isEmpty(props.action.errors)) { + if (draftMessage !== undefined || !isEmptyObject(action.errors)) { return; } @@ -311,11 +339,11 @@ function ReportActionItem(props) { CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, selection, - popoverAnchorRef, - props.report.reportID, - props.action.reportActionID, + popoverAnchorRef.current, + report.reportID, + action.reportActionID, originalReportID, - props.draftMessage, + draftMessage ?? '', () => setIsContextMenuActive(true), toggleContextMenuFromActiveReportAction, ReportUtils.isArchivedRoom(originalReport), @@ -324,168 +352,184 @@ function ReportActionItem(props) { false, [], false, - setIsEmojiPickerActive, + setIsEmojiPickerActive as () => void, ); }, - [props.draftMessage, props.action, props.report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], + [draftMessage, action, report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID], ); // Handles manual scrolling to the bottom of the chat when the last message is an actionable mention whisper and it's resolved. // This fixes an issue where InvertedFlatList fails to auto scroll down and results in an empty space at the bottom of the chat in IOS. useEffect(() => { - if (props.index !== 0 || !ReportActionsUtils.isActionableMentionWhisper(props.action)) { + if (index !== 0 || !ReportActionsUtils.isActionableMentionWhisper(action)) { return; } - if (prevActionResolution !== lodashGet(props.action, 'originalMessage.resolution', null)) { - reportScrollManager.scrollToIndex(props.index); + if (ReportActionsUtils.isActionableMentionWhisper(action) && prevActionResolution !== (action.originalMessage.resolution ?? null)) { + reportScrollManager.scrollToIndex(index); } - }, [props.index, props.action, prevActionResolution, reportScrollManager]); + }, [index, action, prevActionResolution, reportScrollManager]); const toggleReaction = useCallback( - (emoji) => { - Report.toggleEmojiReaction(props.report.reportID, props.action, emoji, props.emojiReactions); + (emoji: Emoji) => { + Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions); }, - [props.report, props.action, props.emojiReactions], + [report, action, emojiReactions], ); const contextValue = useMemo( () => ({ - anchor: popoverAnchorRef, - report: props.report, - action: props.action, + anchor: popoverAnchorRef.current, + report, + action, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, }), - [props.report, props.action, toggleContextMenuFromActiveReportAction], + [report, action, toggleContextMenuFromActiveReportAction], ); - const actionableItemButtons = useMemo(() => { - if (!(ReportActionsUtils.isActionableMentionWhisper(props.action) && !lodashGet(props.action, 'originalMessage.resolution', null))) { + const actionableItemButtons: ActionableItem[] = useMemo(() => { + const isWhisperResolution = (action?.originalMessage as OriginalMessageActionableMentionWhisper['originalMessage'])?.resolution !== null; + const isJoinChoice = (action?.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage'])?.choice === ''; + + if (!((ReportActionsUtils.isActionableMentionWhisper(action) && isWhisperResolution) || (ReportActionsUtils.isActionableJoinRequest(action) && isJoinChoice))) { return []; } + + if (ReportActionsUtils.isActionableJoinRequest(action)) { + return [ + { + text: 'actionableMentionJoinWorkspaceOptions.accept', + key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT}`, + onPress: () => Policy.acceptJoinRequest(report.reportID, action), + isPrimary: true, + }, + { + text: 'actionableMentionJoinWorkspaceOptions.decline', + key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE}`, + onPress: () => Policy.declineJoinRequest(report.reportID, action), + }, + ]; + } return [ { text: 'actionableMentionWhisperOptions.invite', - key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`, - onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE), + key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`, + onPress: () => Report.resolveActionableMentionWhisper(report.reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE), isPrimary: true, }, { text: 'actionableMentionWhisperOptions.nothing', - key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`, - onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING), + key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`, + onPress: () => Report.resolveActionableMentionWhisper(report.reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING), }, ]; - }, [props.action, props.report.reportID]); + }, [action, report.reportID]); /** * Get the content of ReportActionItem - * @param {Boolean} hovered whether the ReportActionItem is hovered - * @param {Boolean} isWhisper whether the report action is a whisper - * @param {Boolean} hasErrors whether the report action has any errors - * @returns {Object} child component(s) + * @param hovered whether the ReportActionItem is hovered + * @param isWhisper whether the report action is a whisper + * @param hasErrors whether the report action has any errors + * @returns child component(s) */ - const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => { + const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false): React.JSX.Element => { let children; // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - originalMessage && + isIOUReport(action) && + action.originalMessage && // For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message - (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) + (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney) ) { // There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID - const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; + const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0'; children = ( ); - } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { - children = ( + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) { + children = ReportUtils.isClosedExpenseReportWithNoExpenses(iouReport) ? ( + ${translate('parentReportAction.deletedReport')}`} /> + ) : ( ); - } else if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED - ) { - children = ; - } else if (ReportActionsUtils.isCreatedTaskReportAction(props.action)) { + } else if (ReportActionsUtils.isTaskAction(action)) { + children = ; + } else if (ReportActionsUtils.isCreatedTaskReportAction(action)) { children = ( ); - } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); - const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[report.ownerAccountID ?? -1]); + const paymentType = action.originalMessage.paymentType ?? ''; - const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); + const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(report.reportID) && !ReportUtils.isSettled(report.reportID); const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY; const shouldShowEnableWalletButton = - isSubmitterOfUnsettledReport && - (_.isEmpty(props.userWallet) || props.userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER) && - paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; + isSubmitterOfUnsettledReport && (isEmptyObject(userWallet) || userWallet?.tierName === CONST.WALLET.TIER_NAME.SILVER) && paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; children = ( <> {shouldShowAddCreditBankAccountButton && ( )} @@ -535,49 +579,46 @@ function ReportActionItem(props) { for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} + {actionableItemButtons.length > 0 && } ) : ( )} ); } - const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); + const numberOfThreadReplies = action.childVisibleActionCount ?? 0; - const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); - const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); - const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID); + const oldestFourAccountIDs = + action.childOldestFourAccountIDs + ?.split(',') + .map((accountID) => Number(accountID)) + .filter((accountID): accountID is number => typeof accountID === 'number') ?? []; + const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} - {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && ( - - !_.isEmpty(item))} /> + {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( + + !isEmptyObject(item))} /> )} - {!ReportActionsUtils.isMessageDeleted(props.action) && ( + {!ReportActionsUtils.isMessageDeleted(action) && ( { if (Session.isAnonymousUser()) { @@ -598,9 +639,9 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( { + const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => { const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors); - if (!_.isUndefined(props.draftMessage)) { + if (draftMessage !== undefined) { return {content}; } - if (!props.displayAsGroup) { + if (!displayAsGroup) { return ( item === moderationDecision) && + !ReportActionsUtils.isPendingRemove(action) } > {content} @@ -648,23 +689,23 @@ function ReportActionItem(props) { return {content}; }; - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; if (ReportActionsUtils.isTransactionThread(parentReportAction)) { const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction); if (ReportActionsUtils.isDeletedParentAction(parentReportAction) || isReversedTransaction) { return ( - + - - + + ${props.translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} + html={`${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} /> @@ -676,26 +717,26 @@ function ReportActionItem(props) { return ( ); } - if (ReportUtils.isTaskReport(props.report)) { - if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { + if (ReportUtils.isTaskReport(report)) { + if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { return ( - + - - + + - ${props.translate('parentReportAction.deletedTask')}`} /> + ${translate('parentReportAction.deletedTask')}`} /> @@ -704,44 +745,44 @@ function ReportActionItem(props) { ); } return ( - + - + ); } - if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { return ( - - {transactionThreadReport && !_.isEmpty(transactionThreadReport) ? ( + + {transactionThreadReport && !lodashIsEmpty(transactionThreadReport) ? ( <> - {transactionCurrency !== props.report.currency && ( + {transactionCurrency !== report.currency && ( )} ) : ( )} @@ -750,96 +791,94 @@ function ReportActionItem(props) { return ( ); } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - return ; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return ; } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { return ( ); } // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet - if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(props.report, 'isWaitingOnBankAccount', false) && - originalMessage && - originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && - !isSendingMoney - ) { + if (isIOUReport(action) && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) { return null; } // if action is actionable mention whisper and resolved by user, then we don't want to render anything - if (ReportActionsUtils.isActionableMentionWhisper(props.action) && lodashGet(props.action, 'originalMessage.resolution', null)) { + if (ReportActionsUtils.isActionableMentionWhisper(action) && (action.originalMessage.resolution ?? null)) { return null; } // We currently send whispers to all report participants and hide them in the UI for users that shouldn't see them. // This is a temporary solution needed for comment-linking. // The long term solution will leverage end-to-end encryption and only targeted users will be able to decrypt. - if (ReportActionsUtils.isWhisperActionTargetedToOthers(props.action)) { + if (ReportActionsUtils.isWhisperActionTargetedToOthers(action)) { return null; } - const hasErrors = !_.isEmpty(props.action.errors); - const whisperedToAccountIDs = props.action.whisperedToAccountIDs || []; + const hasErrors = !isEmptyObject(action.errors); + const whisperedToAccountIDs = action.whisperedToAccountIDs ?? []; const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); - const whisperedToPersonalDetails = isWhisper ? _.filter(personalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)) : []; + const whisperedToPersonalDetails = isWhisper + ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) + : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPress={onPress} + style={[action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]} + onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors} + preventDefaultContextMenu={draftMessage === undefined && !hasErrors} withoutFocusOnSecondaryInteraction - accessibilityLabel={props.translate('accessibilityHints.chatMessage')} + accessibilityLabel={translate('accessibilityHints.chatMessage')} + accessible > {(hovered) => ( - {props.shouldDisplayNewMarker && } + {shouldDisplayNewMarker && } - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} + onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing pendingAction={ - !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') + draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) } - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} - errors={ErrorUtils.getLatestErrorMessageField(props.action)} + shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} + errors={ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)} + needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} shouldDisableStrikeThrough > {isWhisper && ( @@ -852,11 +891,11 @@ function ReportActionItem(props) { /> - {props.translate('reportActionContextMenu.onlyVisible')} + {translate('reportActionContextMenu.onlyVisible')}   )} - {renderReportActionItem(hovered || isReportActionLinked, isWhisper, hasErrors)} + {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)} )} - + {/* @ts-expect-error TODO check if there is a field on the reportAction object */} + ); } -ReportActionItem.propTypes = propTypes; -ReportActionItem.defaultProps = defaultProps; - -export default compose( - withWindowDimensions, - withLocalize, - withNetwork(), - withBlockedFromConcierge({propName: 'blockedFromConcierge'}), - withReportActionsDrafts({ - propName: 'draftMessage', - transformValue: (drafts, props) => { - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); - const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']); - }, - }), - withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, - }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - iouReport: { - key: ({action}) => { - const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); - return iouReportID ? `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}` : undefined; - }, - initialValue: {}, - }, - policyReportFields: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), - initialValue: [], +export default withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, + }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + iouReport: { + key: ({action}) => { + const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); + return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID ?? ''}`; }, - policy: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined), - initialValue: {}, - }, - emojiReactions: { - key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, - initialValue: {}, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || 0}`, - canEvict: false, - }, - reportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID || 0}`, - canEvict: false, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - }), -)( + initialValue: {} as OnyxTypes.Report, + }, + policyReportFields: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID ?? ''}`, + initialValue: {}, + }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID ?? ''}`, + initialValue: {} as OnyxTypes.Policy, + }, + emojiReactions: { + key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, + initialValue: {}, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? 0}`, + canEvict: false, + }, + reportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID || 0}`, + canEvict: false, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, +})( memo(ReportActionItem, (prevProps, nextProps) => { - const prevParentReportAction = prevProps.parentReportActions[prevProps.report.parentReportActionID]; - const nextParentReportAction = nextProps.parentReportActions[nextProps.report.parentReportActionID]; + const prevParentReportAction = prevProps.parentReportActions?.[prevProps.report.parentReportActionID ?? '']; + const nextParentReportAction = nextProps.parentReportActions?.[nextProps.report.parentReportActionID ?? '']; return ( prevProps.displayAsGroup === nextProps.displayAsGroup && - prevProps.draftMessage === nextProps.draftMessage && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && - _.isEqual(prevProps.reports, nextProps.reports) && - _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && - _.isEqual(prevProps.action, nextProps.action) && - _.isEqual(prevProps.iouReport, nextProps.iouReport) && - _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && - _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && - _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) && - lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') && - lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') && - lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') && - lodashGet(prevProps.report, 'parentReportActionID') === lodashGet(nextProps.report, 'parentReportActionID') && - prevProps.translate === nextProps.translate && + lodashIsEqual(prevProps.reports, nextProps.reports) && + lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) && + lodashIsEqual(prevProps.action, nextProps.action) && + lodashIsEqual(prevProps.iouReport, nextProps.iouReport) && + lodashIsEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && + lodashIsEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && + lodashIsEqual(prevProps.report.errorFields, nextProps.report.errorFields) && + prevProps.report?.statusNum === nextProps.report?.statusNum && + prevProps.report?.stateNum === nextProps.report?.stateNum && + prevProps.report?.parentReportID === nextProps.report?.parentReportID && + prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID && // TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) && prevProps.action.actionName === nextProps.action.actionName && @@ -965,15 +986,15 @@ export default compose( ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) && prevProps.report.managerID === nextProps.report.managerID && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && - lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && - lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && + prevProps.report?.total === nextProps.report?.total && + prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) && - _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) && - _.isEqual(prevProps.policy, nextProps.policy) && - _.isEqual(prevParentReportAction, nextParentReportAction) && - _.isEqual(prevProps.reportActions, nextProps.reportActions) && - _.isEqual(prevProps.transactions, nextProps.transactions) + lodashIsEqual(prevProps.policyReportFields, nextProps.policyReportFields) && + lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) && + lodashIsEqual(prevProps.policy, nextProps.policy) && + lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && + lodashIsEqual(prevProps.transactions, nextProps.transactions) && + lodashIsEqual(prevParentReportAction, nextParentReportAction) ); }), ); diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx index 35141a42b726..a28f2af24448 100644 --- a/src/pages/home/report/ReportActionItemBasicMessage.tsx +++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx @@ -5,7 +5,7 @@ import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -type ReportActionItemBasicMessageProps = ChildrenProps & { +type ReportActionItemBasicMessageProps = Partial & { message: string; }; diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 95578c10e816..4fe52f6adf41 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -35,7 +35,7 @@ type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & { /** The id of the policy */ // eslint-disable-next-line react/no-unused-prop-types - policyID: string; + policyID: string | undefined; }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index e16d94eb7db7..04391bb19cd5 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -70,6 +70,7 @@ const MUTED_ACTIONS = [ CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.APPROVED, CONST.REPORT.ACTIONS.TYPE.MOVED, + CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST, ] as ActionName[]; function ReportActionItemFragment({ diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 2c9a4cbd21e8..fbf2da69aa31 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -5,6 +5,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Keyboard, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -58,7 +59,7 @@ type ReportActionItemMessageEditProps = { shouldDisableEmojiPicker?: boolean; /** Stores user's preferred skin tone */ - preferredSkinTone?: number; + preferredSkinTone?: OnyxEntry; }; // native ids @@ -69,7 +70,7 @@ const isMobileSafari = Browser.isMobileSafari(); function ReportActionItemMessageEdit( {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, - forwardedRef: ForwardedRef, + forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index af1c4e85104e..4a041fc495c0 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -83,7 +83,6 @@ function ReportActionItemParentAction({report, index = 0, shouldHideThreadDivide onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} > Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))} report={ancestor.report} action={ancestor.reportAction} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 741422cc7e82..696cd7a7d850 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -29,7 +30,7 @@ import ReportActionItemFragment from './ReportActionItemFragment'; type ReportActionItemSingleProps = Partial & { /** All the data of the action */ - action: ReportAction; + action: OnyxEntry; /** Styles for the outermost View */ wrapperStyle?: StyleProp; @@ -38,7 +39,7 @@ type ReportActionItemSingleProps = Partial & { report: Report; /** IOU Report for this action, if any */ - iouReport?: Report; + iouReport?: OnyxEntry; /** Show header for action */ showHeader?: boolean; @@ -77,12 +78,12 @@ function ReportActionItemSingle({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID; + const actorAccountID = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action?.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]); + const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action?.actionName, iouReport]); const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); @@ -90,7 +91,7 @@ function ReportActionItemSingle({ displayName = ReportUtils.getPolicyName(report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(report); - } else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) { + } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. const delegateDetails = personalDetails[action.delegateAccountID]; @@ -141,7 +142,7 @@ function ReportActionItemSingle({ text: displayName, }, ] - : action.person; + : action?.person; const reportID = report?.reportID; const iouReportID = iouReport?.reportID; @@ -155,14 +156,14 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); + showUserDetails(action?.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); } - }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); + }, [isWorkspaceActor, reportID, actorAccountID, action?.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( () => CONST.RESTRICTED_ACCOUNT_IDS.includes(actorAccountID ?? 0) || - (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), + (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action?.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), [action, isWorkspaceActor, actorAccountID], ); @@ -189,7 +190,7 @@ function ReportActionItemSingle({ return ( @@ -237,13 +238,13 @@ function ReportActionItemSingle({ {personArray?.map((fragment, index) => ( ))} @@ -255,7 +256,7 @@ function ReportActionItemSingle({ >{`${status?.emojiCode}`} )} - +
) : null} {children} diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx index f7c7e5fcf91d..c0dbe2a3825d 100644 --- a/src/pages/home/report/ReportActionItemThread.tsx +++ b/src/pages/home/report/ReportActionItemThread.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; @@ -26,7 +27,7 @@ type ReportActionItemThreadProps = { isHovered: boolean; /** The function that should be called when the thread is LongPressed or right-clicked */ - onSecondaryInteraction: () => void; + onSecondaryInteraction: (event: GestureResponderEvent | MouseEvent) => void; }; function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) { diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 5e9d863dd62d..ca3ee7d2ab6a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -194,7 +194,7 @@ function ReportActionsView(props) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments - Report.getOlderActions(reportID, oldestReportAction.reportActionID); + Report.getOlderActions(reportID); }, [props.isLoadingOlderReportActions, props.network.isOffline, oldestReportAction, reportID]); /** @@ -223,10 +223,9 @@ function ReportActionsView(props) { return; } - const newestReportAction = _.first(props.reportActions); - Report.getNewerActions(reportID, newestReportAction.reportActionID); + Report.getNewerActions(reportID); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID, hasNewestReportAction], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, hasNewestReportAction], ); /** diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx index a9e284329421..7151cc84e735 100644 --- a/src/pages/home/sidebar/AllSettingsScreen.tsx +++ b/src/pages/home/sidebar/AllSettingsScreen.tsx @@ -1,11 +1,11 @@ import React, {useMemo} from 'react'; -import {ScrollView} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 8d7272df63e9..62b1adf1fb8c 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -62,6 +63,7 @@ const defaultProps = { function MoneyRequestSelectorPage(props) { const styles = useThemeStyles(); const [isDraggingOver, setIsDraggingOver] = useState(false); + const {canUseP2PDistanceRequests} = usePermissions(); const iouType = lodashGet(props.route, 'params.iouType', ''); const reportID = lodashGet(props.route, 'params.reportID', ''); @@ -75,7 +77,7 @@ function MoneyRequestSelectorPage(props) { const isFromGlobalCreate = !reportID; const isExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); const isExpenseReport = ReportUtils.isExpenseReport(props.report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; const resetMoneyRequestInfo = () => { const moneyRequestID = `${iouType}${reportID}`; diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 8e50577ede1f..b1ae257b792f 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -13,6 +13,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -80,6 +81,7 @@ function IOURequestStartPage({ }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); + const {canUseP2PDistanceRequests} = usePermissions(); const isFromGlobalCreate = _.isEmpty(report.reportID); useFocusEffect( @@ -102,12 +104,12 @@ function IOURequestStartPage({ if (transaction.reportID === reportID) { return; } - IOU.initMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current); - }, [transaction, reportID, iouType, isFromGlobalCreate]); + IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, transactionRequestType.current); + }, [transaction, policy, reportID, iouType, isFromGlobalCreate]); const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); @@ -124,10 +126,10 @@ function IOURequestStartPage({ if (iouType === CONST.IOU.TYPE.SPLIT && transaction.isFromGlobalCreate) { IOU.updateMoneyRequestTypeParams(navigation.getState().routes, CONST.IOU.TYPE.REQUEST, newIouType); } - IOU.initMoneyRequest(reportID, isFromGlobalCreate, newIouType); + IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, newIouType); transactionRequestType.current = newIouType; }, - [previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate], + [policy, previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate], ); if (!transaction.transactionID) { diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 95dda131eab7..fb3a4d9457d5 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -90,6 +91,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); + const {canUseP2PDistanceRequests} = usePermissions(); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -120,18 +122,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // sees the option to request money from their admin on their own Workspace Chat. iouType === CONST.IOU.TYPE.REQUEST, - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, {}, [], false, {}, [], - - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, ); @@ -182,7 +180,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); + }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, canUseP2PDistanceRequests, translate]); /** * Adds a single participant to the request @@ -257,7 +255,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; + const isAllowedToSplit = canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; const handleConfirmSelection = useCallback(() => { if (shouldShowSplitBillErrorMessage) { diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js index af1de64f8930..1b53dab12fa3 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.js +++ b/src/pages/iou/request/step/IOURequestStepTag.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import categoryPropTypes from '@components/categoryPropTypes'; import TagPicker from '@components/TagPicker'; @@ -11,7 +11,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import {canEditMoneyRequest} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; @@ -78,10 +80,12 @@ function IOURequestStepTag({ const tag = TransactionUtils.getTag(transaction, tagIndex); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); const parentReportAction = parentReportActions[report.parentReportActionID]; + const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = isEditing && !canEditMoneyRequest(parentReportAction); + const shouldShowNotFoundPage = !shouldShowTag || (isEditing && !canEditMoneyRequest(parentReportAction)); const navigateBack = () => { Navigation.goBack(backTo); diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx index cb1f73ae2207..55bf77e9ae88 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx @@ -1,11 +1,12 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import type {ValueOf} from 'type-fest'; import BigNumberPad from '@components/BigNumberPad'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; +import ScrollView from '@components/ScrollView'; import TextInputWithCurrencySymbol from '@components/TextInputWithCurrencySymbol'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..1ad6488aeee9 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -94,6 +95,7 @@ function MoneyRequestParticipantsSelector({ const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); + const {canUseP2PDistanceRequests} = usePermissions(); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -113,8 +115,7 @@ function MoneyRequestParticipantsSelector({ // sees the option to request money from their admin on their own Workspace Chat. iouType === CONST.IOU.TYPE.REQUEST, - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - !isDistanceRequest, + canUseP2PDistanceRequests || !isDistanceRequest, false, {}, [], @@ -123,7 +124,7 @@ function MoneyRequestParticipantsSelector({ [], // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - !isDistanceRequest, + canUseP2PDistanceRequests || !isDistanceRequest, true, ); return { @@ -131,7 +132,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); + }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]); /** * Returns the sections needed for the OptionsSelector @@ -272,7 +273,7 @@ function MoneyRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; + const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && iouType !== CONST.IOU.TYPE.SEND; const handleConfirmSelection = useCallback(() => { if (shouldShowSplitBillErrorMessage) { diff --git a/src/pages/settings/AboutPage/AboutPage.tsx b/src/pages/settings/AboutPage/AboutPage.tsx index 3346b044ceca..0c087b2c93d6 100644 --- a/src/pages/settings/AboutPage/AboutPage.tsx +++ b/src/pages/settings/AboutPage/AboutPage.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useMemo, useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, Text as RNText, StyleProp, ViewStyle} from 'react-native'; import DeviceInfo from 'react-native-device-info'; @@ -9,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; diff --git a/src/pages/settings/AppDownloadLinks.tsx b/src/pages/settings/AppDownloadLinks.tsx index 352b3772923a..e4165178ff2f 100644 --- a/src/pages/settings/AppDownloadLinks.tsx +++ b/src/pages/settings/AppDownloadLinks.tsx @@ -1,11 +1,11 @@ import React, {useRef} from 'react'; -import {ScrollView} from 'react-native'; import type {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import type {MenuItemProps} from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx index 7459819afd99..14739c4ffc52 100644 --- a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx +++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect} from 'react'; -import {View} from 'react-native'; +import {NativeModules, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Icon from '@components//Icon'; @@ -84,6 +84,12 @@ function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitS text={translate('exitSurvey.goToExpensifyClassic')} onPress={() => { ExitSurvey.switchToOldDot(); + + if (NativeModules.HybridAppModule) { + NativeModules.HybridAppModule.closeReactNativeApp(); + return; + } + Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX); }} isLoading={isLoading ?? false} diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index b29fd600ae16..2f2343027cf0 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,7 +1,7 @@ import {useNavigationState} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; -import {NativeModules, View} 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'; @@ -174,23 +174,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa ], }; - if (NativeModules.HybridAppModule) { - const hybridAppMenuItems: MenuData[] = [ - { - translationKey: 'initialSettingsPage.returnToClassic' as const, - icon: Expensicons.RotateLeft, - shouldShowRightIcon: true, - iconRight: Expensicons.NewWindow, - action: () => { - NativeModules.HybridAppModule.closeReactNativeApp(); - }, - }, - ...defaultMenu.items, - ].filter((item) => item.translationKey !== 'initialSettingsPage.signOut' && item.translationKey !== 'exitSurvey.goToExpensifyClassic'); - - return {sectionStyle: styles.accountSettingsSectionContainer, sectionTranslationKey: 'initialSettingsPage.account', items: hybridAppMenuItems}; - } - return defaultMenu; }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, signOut]); diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.js index 0fd6121fe512..36a26ccffaa2 100755 --- a/src/pages/settings/Preferences/PreferencesPage.js +++ b/src/pages/settings/Preferences/PreferencesPage.js @@ -1,13 +1,14 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Switch from '@components/Switch'; import Text from '@components/Text'; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 18589beb6353..2ba4fc33580b 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -1,7 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, Keyboard, ScrollView, View} from 'react-native'; +import {InteractionManager, Keyboard, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -13,6 +13,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 5d150e782c44..3851ef7153fb 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -1,7 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -11,6 +11,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 968d9e502806..2fa133f41616 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -10,6 +10,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemGroup from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; diff --git a/src/pages/settings/Report/ReportSettingsPage.tsx b/src/pages/settings/Report/ReportSettingsPage.tsx index 54057f7c05bb..383cbbcb0833 100644 --- a/src/pages/settings/Report/ReportSettingsPage.tsx +++ b/src/pages/settings/Report/ReportSettingsPage.tsx @@ -1,12 +1,13 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index 8600c9e08471..01563e586792 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -1,11 +1,12 @@ import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx index d6c7a1abcd4f..b4c1bc249c81 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import {ActivityIndicator, ScrollView, View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; @@ -7,6 +7,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx index 59c145f9e348..ad9a4060af45 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx @@ -1,8 +1,9 @@ import React, {useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx index d9998c777f3b..58e7d98d69de 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; import Button from '@components/Button'; @@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import {useSession} from '@components/OnyxProvider'; import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle'; import QRCode from '@components/QRCode'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index a8b676f6c379..097b2cf28ed0 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx index 93ead17e9523..85b7bef0550c 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.tsx +++ b/src/pages/settings/Wallet/TransferBalancePage.tsx @@ -1,5 +1,5 @@ import React, {useEffect} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index b9f49049d51a..88236e06f9a9 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -2,7 +2,7 @@ import _ from 'lodash'; import type {ForwardedRef, RefObject} from 'react'; import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; -import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native'; +import {ActivityIndicator, Dimensions, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import Button from '@components/Button'; @@ -18,6 +18,7 @@ import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Popover from '@components/Popover'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -74,7 +75,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi }); const addPaymentMethodAnchorRef = useRef(null); - const paymentMethodButtonRef = useRef(null); + const paymentMethodButtonRef = useRef(null); const [anchorPosition, setAnchorPosition] = useState({ anchorPositionHorizontal: 0, anchorPositionVertical: 0, @@ -163,7 +164,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi setShouldShowDefaultDeleteMenu(false); return; } - paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLElement; + paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement; // The delete/default menu if (accountType) { diff --git a/src/pages/signin/SAMLSignInPage/index.tsx b/src/pages/signin/SAMLSignInPage/index.tsx index 701c2917bea6..1ff9d02672be 100644 --- a/src/pages/signin/SAMLSignInPage/index.tsx +++ b/src/pages/signin/SAMLSignInPage/index.tsx @@ -7,7 +7,7 @@ import type {SAMLSignInPageOnyxProps, SAMLSignInPageProps} from './types'; function SAMLSignInPage({credentials}: SAMLSignInPageProps) { useEffect(() => { - window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self'); + window.location.replace(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`); }, [credentials?.login]); return ; diff --git a/src/pages/signin/SignInPageLayout/index.tsx b/src/pages/signin/SignInPageLayout/index.tsx index b65da7eba0a5..3532c17181db 100644 --- a/src/pages/signin/SignInPageLayout/index.tsx +++ b/src/pages/signin/SignInPageLayout/index.tsx @@ -1,8 +1,11 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {ScrollView as RNScrollView} from 'react-native'; +import {View} from 'react-native'; import SignInGradient from '@assets/images/home-fade-gradient.svg'; import ImageSVG from '@components/ImageSVG'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -38,7 +41,7 @@ function SignInPageLayout( const StyleUtils = useStyleUtils(); const {preferredLocale} = useLocalize(); const {top: topInsets, bottom: bottomInsets} = useSafeAreaInsets(); - const scrollViewRef = useRef(null); + const scrollViewRef = useRef(null); const prevPreferredLocale = usePrevious(preferredLocale); const {windowHeight, isMediumScreenWidth, isLargeScreenWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js index f77285190e62..352c08115114 100644 --- a/src/pages/tasks/NewTaskPage.js +++ b/src/pages/tasks/NewTaskPage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -155,14 +156,7 @@ function NewTaskPage(props) { Navigation.goBack(ROUTES.NEW_TASK_DETAILS); }} /> - + ; /** Grab the Share destination of the Task */ - task: PropTypes.shape({ - /** Share destination of the Task */ - shareDestination: PropTypes.string, - - /** The task report if it's currently being edited */ - report: reportPropTypes, - }), - - /** The policy of root parent report */ - rootParentReportPolicy: PropTypes.shape({ - /** The role of current user */ - role: PropTypes.string, - }), + task: OnyxEntry; }; -const defaultProps = { - reports: {}, - task: {}, - rootParentReportPolicy: {}, +type UseOptions = { + reports: OnyxCollection; }; -function useOptions({reports}) { +type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps; + +function useOptions({reports}: UseOptions) { const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); @@ -78,7 +69,7 @@ function useOptions({reports}) { ); const headerMessage = OptionsListUtils.getHeaderMessage( - (recentReports.length || 0 + personalDetails.length || 0) !== 0 || currentUserOption, + (recentReports?.length || 0) + (personalDetails?.length || 0) !== 0 || Boolean(currentUserOption), Boolean(userToInvite), debouncedSearchValue, ); @@ -99,20 +90,20 @@ function useOptions({reports}) { return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; } -function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { +function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { const styles = useThemeStyles(); - const route = useRoute(); + const route = useRoute>(); const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task}); + const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports}); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); }; - const report = useMemo(() => { - if (!route.params || !route.params.reportID) { + const report: OnyxEntry = useMemo(() => { + if (!route.params?.reportID) { return null; } if (report && !ReportUtils.isTaskReport(report)) { @@ -120,7 +111,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { Navigation.dismissModal(report.reportID); }); } - return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`]; + return reports?.[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID}`] ?? null; }, [reports, route]); const sections = useMemo(() => { @@ -155,17 +146,29 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { if (userToInvite) { sectionsList.push({ + title: '', data: [userToInvite], shouldShow: true, indexOffset, }); } - return sectionsList; - }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]); + return sectionsList.map((section) => ({ + ...section, + data: section.data.map((option) => ({ + ...option, + text: option.text ?? '', + alternateText: option.alternateText ?? undefined, + keyForList: option.keyForList ?? '', + isDisabled: option.isDisabled ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + })); + }, [currentUserOption, personalDetails, recentReports, translate, userToInvite]); const selectReport = useCallback( - (option) => { + (option: ListItem) => { if (!option) { return; } @@ -173,25 +176,35 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { // Check to see if we're editing a task and if so, update the assignee if (report) { if (option.accountID !== report.managerID) { - const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option)); + const assigneeChatReport = TaskActions.setAssigneeValue( + option?.login ?? '', + option?.accountID ?? -1, + report.reportID, + OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? ''}), + ); // Pass through the selected assignee - Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport); + TaskActions.editTaskAssignee(report, session?.accountID ?? 0, option?.login ?? '', option?.accountID, assigneeChatReport); } Navigation.dismissModal(report.reportID); // If there's no report, we're creating a new task } else if (option.accountID) { - Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option)); + TaskActions.setAssigneeValue( + option?.login ?? '', + option.accountID, + task?.shareDestination ?? '', + OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? undefined}), + ); Navigation.goBack(ROUTES.NEW_TASK); } }, - [session.accountID, task.shareDestination, report], + [session?.accountID, task?.shareDestination, report], ); - const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); + const handleBackButtonPress = useCallback(() => (route.params?.reportID ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', '')); + const canModifyTask = TaskActions.canModifyTask(report, currentUserPersonalDetails.accountID); const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( @@ -199,7 +212,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { includeSafeAreaPaddingBottom={false} testID={TaskAssigneeSelectorModal.displayName} > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd}) => ( @@ -225,26 +237,14 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { } TaskAssigneeSelectorModal.displayName = 'TaskAssigneeSelectorModal'; -TaskAssigneeSelectorModal.propTypes = propTypes; -TaskAssigneeSelectorModal.defaultProps = defaultProps; -export default compose( - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - task: { - key: ONYXKEYS.TASK, - }, - }), - withOnyx({ - rootParentReportPolicy: { - key: ({reports, route}) => { - const report = reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID || '0'}`]; - const rootParentReport = ReportUtils.getRootParentReport(report); - return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`; - }, - selector: (policy) => lodashPick(policy, ['role']), - }, - }), -)(TaskAssigneeSelectorModal); +const TaskAssigneeSelectorModalWithOnyx = withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + task: { + key: ONYXKEYS.TASK, + }, +})(TaskAssigneeSelectorModal); + +export default withCurrentUserPersonalDetails(TaskAssigneeSelectorModalWithOnyx); diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.tsx similarity index 61% rename from src/pages/tasks/TaskDescriptionPage.js rename to src/pages/tasks/TaskDescriptionPage.tsx index b8b48abd09ff..e08d6380bb18 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -2,53 +2,43 @@ import {useFocusEffect} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/EditTaskForm'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, - - /* Onyx Props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - report: {}, -}; +type TaskDescriptionPageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps; const parser = new ExpensiMark(); -function TaskDescriptionPage(props) { + +function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescriptionPageProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); - /** - * @param {Object} values - form input values passed by the Form component - * @returns {Boolean} - */ - const validate = useCallback((values) => { + const validate = useCallback((values: FormOnyxValues): FormInputErrors => { const errors = {}; - if (values.description.length > CONST.DESCRIPTION_LIMIT) { + if (values?.description && values.description?.length > CONST.DESCRIPTION_LIMIT) { ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]); } @@ -56,30 +46,30 @@ function TaskDescriptionPage(props) { }, []); const submit = useCallback( - (values) => { - // props.report.description might contain CRLF from the server - if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(props.report.description)) { + (values: FormOnyxValues) => { + // report.description might contain CRLF from the server + if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(report?.description) && !isEmptyObject(report)) { // Set the description of the report in the store and then call EditTask API // to update the description of the report on the server - Task.editTask(props.report, {description: values.description}); + Task.editTask(report, {description: values.description}); } - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }, - [props], + [report], ); - if (!ReportUtils.isTaskReport(props.report)) { + if (!ReportUtils.isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }); } - const inputRef = useRef(null); - const focusTimeoutRef = useRef(null); + const inputRef = useRef(null); + const focusTimeoutRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(props.report); - const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen); + const isOpen = ReportUtils.isOpenTaskReport(report); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); useFocusEffect( useCallback(() => { @@ -104,13 +94,13 @@ function TaskDescriptionPage(props) { testID={TaskDescriptionPage.displayName} > - + @@ -119,14 +109,14 @@ function TaskDescriptionPage(props) { role={CONST.ROLE.PRESENTATION} inputID={INPUT_IDS.DESCRIPTION} name={INPUT_IDS.DESCRIPTION} - label={props.translate('newTaskPage.descriptionOptional')} - accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} - defaultValue={parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || '')} - ref={(el) => { - if (!el) { + label={translate('newTaskPage.descriptionOptional')} + accessibilityLabel={translate('newTaskPage.descriptionOptional')} + defaultValue={parser.htmlToMarkdown((report && parser.replace(report?.description ?? '')) || '')} + ref={(element: AnimatedTextInputRef) => { + if (!element) { return; } - inputRef.current = el; + inputRef.current = element; updateMultilineInputRange(inputRef.current); }} autoGrowHeight @@ -140,17 +130,8 @@ function TaskDescriptionPage(props) { ); } -TaskDescriptionPage.propTypes = propTypes; -TaskDescriptionPage.defaultProps = defaultProps; TaskDescriptionPage.displayName = 'TaskDescriptionPage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - }), -)(TaskDescriptionPage); +const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskDescriptionPage); + +export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails); diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx similarity index 62% rename from src/pages/tasks/TaskShareDestinationSelectorModal.js rename to src/pages/tasks/TaskShareDestinationSelectorModal.tsx index b62440b22967..5b56e58752ac 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,9 +1,7 @@ -import keys from 'lodash/keys'; -import reduce from 'lodash/reduce'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -13,51 +11,45 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Report from '@libs/actions/Report'; +import * as ReportActions from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; -const propTypes = { - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** Whether or not we are searching for reports on the server */ - isSearchingForReports: PropTypes.bool, -}; +type TaskShareDestinationSelectorModalOnyxProps = { + reports: OnyxCollection; -const defaultProps = { - reports: {}, - isSearchingForReports: false, + isSearchingForReports: OnyxEntry; }; -const selectReportHandler = (option) => { - if (!option || !option.reportID) { +type TaskShareDestinationSelectorModalProps = TaskShareDestinationSelectorModalOnyxProps; + +const selectReportHandler = (option: unknown) => { + const optionItem = option as ReportUtils.OptionData; + + if (!optionItem || !optionItem?.reportID) { return; } - Task.setShareDestinationValue(option.reportID); + Task.setShareDestinationValue(optionItem?.reportID); Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reports) => - reduce( - keys(reports), - (filtered, reportKey) => { - const report = reports[reportKey]; - if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { - return {...filtered, [reportKey]: report}; - } - return filtered; - }, - {}, - ); +const reportFilter = (reports: OnyxCollection) => + Object.keys(reports ?? {}).reduce((filtered, reportKey) => { + const report: OnyxEntry = reports?.[reportKey] ?? null; + if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { + return {...filtered, [reportKey]: report}; + } + return filtered; + }, {}); -function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { +function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) { const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); @@ -73,13 +65,29 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); - const sections = recentReports && recentReports.length > 0 ? [{data: recentReports, shouldShow: true}] : []; + const sections = + recentReports && recentReports.length > 0 + ? [ + { + data: recentReports.map((option) => ({ + ...option, + text: option.text ?? '', + alternateText: option.alternateText ?? undefined, + keyForList: option.keyForList ?? '', + isDisabled: option.isDisabled ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + shouldShow: true, + }, + ] + : []; return {sections, headerMessage}; }, [personalDetails, reports, debouncedSearchValue]); useEffect(() => { - Report.searchInServer(debouncedSearchValue); + ReportActions.searchInServer(debouncedSearchValue); }, [debouncedSearchValue]); return ( @@ -87,7 +95,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { includeSafeAreaPaddingBottom={false} testID="TaskShareDestinationSelectorModal" > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd}) => ( <> @@ -115,10 +122,8 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { } TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; -TaskShareDestinationSelectorModal.propTypes = propTypes; -TaskShareDestinationSelectorModal.defaultProps = defaultProps; -export default withOnyx({ +export default withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.tsx similarity index 50% rename from src/pages/tasks/TaskTitlePage.js rename to src/pages/tasks/TaskTitlePage.tsx index 370baab7cd89..009983beac3e 100644 --- a/src/pages/tasks/TaskTitlePage.js +++ b/src/pages/tasks/TaskTitlePage.tsx @@ -1,98 +1,85 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/EditTaskForm'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, +type TaskTitlePageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps; - /* Onyx Props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - report: {}, -}; - -function TaskTitlePage(props) { +function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) { const styles = useThemeStyles(); - /** - * @param {Object} values - * @param {String} values.title - * @returns {Object} - An object containing the errors for each inputID - */ - const validate = useCallback((values) => { - const errors = {}; + const {translate} = useLocalize(); + + const validate = useCallback(({title}: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; - if (_.isEmpty(values.title)) { + if (!title) { errors.title = 'newTaskPage.pleaseEnterTaskName'; - } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; }, []); const submit = useCallback( - (values) => { - if (values.title !== props.report.reportName) { + (values: FormOnyxValues) => { + if (values.title !== report?.reportName && !isEmptyObject(report)) { // Set the title of the report in the store and then call EditTask API // to update the title of the report on the server - Task.editTask(props.report, {title: values.title}); + Task.editTask(report, {title: values.title}); } - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }, - [props], + [report], ); - if (!ReportUtils.isTaskReport(props.report)) { + if (!ReportUtils.isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }); } - const inputRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(props.report); - const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen); + const inputRef = useRef(null); + const isOpen = ReportUtils.isOpenTaskReport(report); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( inputRef.current && inputRef.current.focus()} + onEntryTransitionEnd={() => { + inputRef?.current?.focus(); + }} shouldEnableMaxHeight testID={TaskTitlePage.displayName} > {({didScreenTransitionEnd}) => ( - + @@ -101,17 +88,17 @@ function TaskTitlePage(props) { role={CONST.ROLE.PRESENTATION} inputID={INPUT_IDS.TITLE} name={INPUT_IDS.TITLE} - label={props.translate('task.title')} - accessibilityLabel={props.translate('task.title')} - defaultValue={(props.report && props.report.reportName) || ''} - ref={(el) => { - if (!el) { + label={translate('task.title')} + accessibilityLabel={translate('task.title')} + defaultValue={report?.reportName ?? ''} + ref={(element: AnimatedTextInputRef) => { + if (!element) { return; } if (!inputRef.current && didScreenTransitionEnd) { - el.focus(); + element.focus(); } - inputRef.current = el; + inputRef.current = element; }} /> @@ -122,17 +109,8 @@ function TaskTitlePage(props) { ); } -TaskTitlePage.propTypes = propTypes; -TaskTitlePage.defaultProps = defaultProps; TaskTitlePage.displayName = 'TaskTitlePage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - }), -)(TaskTitlePage); +const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskTitlePage); + +export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index d2565022075a..c4f4d6399dbd 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx new file mode 100644 index 000000000000..8167e6fc1ebf --- /dev/null +++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx @@ -0,0 +1,80 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useThemeStyles from '@hooks/useThemeStyles'; +import navigateAfterJoinRequest from '@libs/navigateAfterJoinRequest'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import Navigation from '@navigation/Navigation'; +import type {AuthScreensParamList} from '@navigation/types'; +import * as PolicyAction from '@userActions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Policy} from '@src/types/onyx'; + +type WorkspaceJoinUserPageOnyxProps = { + /** The list of this user's policies */ + policies: OnyxCollection; +}; + +type WorkspaceJoinUserPageRoute = {route: StackScreenProps['route']}; +type WorkspaceJoinUserPageProps = WorkspaceJoinUserPageRoute & WorkspaceJoinUserPageOnyxProps; + +let isJoinLinkUsed = false; + +function WorkspaceJoinUserPage({route, policies}: WorkspaceJoinUserPageProps) { + const styles = useThemeStyles(); + const policyID = route?.params?.policyID; + const inviterEmail = route?.params?.email; + const policy = ReportUtils.getPolicy(policyID); + const isUnmounted = useRef(false); + + useEffect(() => { + if (!isJoinLinkUsed) { + return; + } + Navigation.goBack(undefined, false, true); + }, []); + + useEffect(() => { + if (!policy || !policies || isUnmounted.current || isJoinLinkUsed) { + return; + } + const isPolicyMember = PolicyUtils.isPolicyMember(policyID, policies as Record); + if (isPolicyMember) { + Navigation.goBack(undefined, false, true); + return; + } + PolicyAction.inviteMemberToWorkspace(policyID, inviterEmail); + isJoinLinkUsed = true; + Navigation.isNavigationReady().then(() => { + if (isUnmounted.current) { + return; + } + navigateAfterJoinRequest(); + }); + }, [policy, policyID, policies, inviterEmail]); + + useEffect( + () => () => { + isUnmounted.current = true; + }, + [], + ); + + return ( + + + + ); +} + +WorkspaceJoinUserPage.displayName = 'WorkspaceJoinUserPage'; +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(WorkspaceJoinUserPage); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index e47bc4a09be4..42f29f885c00 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -254,6 +254,23 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se [selectedEmployees, addUser, removeUser], ); + /** Opens the member details page */ + const openMemberDetails = useCallback( + (item: MemberOption) => { + if (!isPolicyAdmin) { + Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); + return; + } + + if (!PolicyUtils.isPaidGroupPolicy(policy)) { + return; + } + + Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID, Navigation.getActiveRoute())); + }, + [isPolicyAdmin, policy, route.params.policyID], + ); + /** * Dismisses the errors on one item */ @@ -417,22 +434,24 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se }, ]; - if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.ADMIN)) { - options.push({ - text: translate('workspace.people.makeMember'), - value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, - icon: Expensicons.User, - onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER), - }); - } + if (PolicyUtils.isPaidGroupPolicy(policy)) { + if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.ADMIN)) { + options.push({ + text: translate('workspace.people.makeMember'), + value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, + icon: Expensicons.User, + onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER), + }); + } - if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.USER)) { - options.push({ - text: translate('workspace.people.makeAdmin'), - value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, - icon: Expensicons.MakeAdmin, - onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN), - }); + if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.USER)) { + options.push({ + text: translate('workspace.people.makeAdmin'), + value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, + icon: Expensicons.MakeAdmin, + onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN), + }); + } } return options; @@ -463,7 +482,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se onPress={inviteUser} text={translate('workspace.invite.member')} icon={Expensicons.Plus} - iconStyles={{transform: [{scale: 0.6}]}} + iconStyles={StyleUtils.getTransformScaleStyle(0.6)} innerStyles={[isSmallScreenWidth && styles.alignItemsCenter]} style={[isSmallScreenWidth && styles.flexGrow1]} /> @@ -523,13 +542,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} headerContent={getHeaderContent()} - onSelectRow={(item) => { - if (!isPolicyAdmin) { - Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); - return; - } - toggleUser(item.accountID); - }} + onSelectRow={openMemberDetails} + onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers))} diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 796f32c343f2..9d90557b1d37 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -1,15 +1,17 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import type {ImageStyle, StyleProp} from 'react-native'; -import {Image, ScrollView, StyleSheet, View} from 'react-native'; +import {Image, StyleSheet, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import Button from '@components/Button'; +import ConfirmModal from '@components/ConfirmModal'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -74,6 +76,19 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi [policy?.avatar, policyName, styles.alignSelfCenter, styles.avatarXLarge], ); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const confirmDeleteAndHideModal = useCallback(() => { + if (!policy?.id || !policyName) { + return; + } + + Policy.deleteWorkspace(policy?.id, policyName); + + PolicyUtils.goBackFromInvalidPolicy(); + + setIsDeleteModalOpen(false); + }, [policy?.id, policyName]); return ( {!readOnly && ( - +