diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 4d2004e6d4c2..7328fb2543ad 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -458,7 +458,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; - [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; + [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; @@ -479,6 +479,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; + [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolation[]; diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index d18704fdfb05..d2143f5b48da 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -8,13 +8,12 @@ import useThemeStyles from '@hooks/useThemeStyles'; import fileDownload from '@libs/fileDownload'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; +import type {ReceiptError} from '@src/types/onyx/Transaction'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; import Text from './Text'; -type ReceiptError = {error?: string; source: string; filename: string}; - type DotIndicatorMessageProps = { /** * In most cases this should just be errors from onxyData @@ -23,7 +22,7 @@ type DotIndicatorMessageProps = { * timestamp: 'message', * } */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; diff --git a/src/components/MessagesRow.tsx b/src/components/MessagesRow.tsx index cfec6fd292e9..7c764ec94fcd 100644 --- a/src/components/MessagesRow.tsx +++ b/src/components/MessagesRow.tsx @@ -6,6 +6,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; +import type {ReceiptError} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DotIndicatorMessage from './DotIndicatorMessage'; import Icon from './Icon'; @@ -15,7 +16,7 @@ import Tooltip from './Tooltip'; type MessagesRowProps = { /** The messages to display */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 1a8f313af267..2fad21fb54ef 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -8,6 +8,7 @@ import mapChildrenFlat from '@libs/mapChildrenFlat'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MessagesRow from './MessagesRow'; @@ -26,7 +27,7 @@ type OfflineWithFeedbackProps = ChildrenProps & { shouldHideOnDelete?: boolean; /** The errors to display */ - errors?: OnyxCommon.Errors | null; + errors?: OnyxCommon.Errors | ReceiptErrors | null; /** Whether we should show the error messages */ shouldShowErrorMessages?: boolean; @@ -84,7 +85,7 @@ function OfflineWithFeedback({ const hasErrors = !isEmptyObject(errors ?? {}); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = omitBy(errors, (e) => e === null); + const errorMessages = omitBy(errors, (e: string | ReceiptError) => e === null); const hasErrorMessages = !isEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.tsx similarity index 58% rename from src/components/ReportActionItem/MoneyRequestAction.js rename to src/components/ReportActionItem/MoneyRequestAction.tsx index 4fca8a0a1aea..ff29bf5b0ee8 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -1,83 +1,66 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import refPropTypes from '@components/refPropTypes'; +import type {OnyxEntry} from 'react-native-onyx'; import RenderHTML from '@components/RenderHTML'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MoneyRequestPreview from './MoneyRequestPreview'; -const propTypes = { +type MoneyRequestActionOnyxProps = { + /** Chat report associated with iouReport */ + chatReport: OnyxEntry; + + /** IOU report data object */ + iouReport: OnyxEntry; + + /** Report actions for this report */ + reportActions: OnyxEntry; +}; + +type MoneyRequestActionProps = MoneyRequestActionOnyxProps & { /** All the data of the action */ - action: PropTypes.shape(reportActionPropTypes).isRequired, + action: OnyxTypes.ReportAction; /** The ID of the associated chatReport */ - chatReportID: PropTypes.string.isRequired, + chatReportID: string; /** The ID of the associated request report */ - requestReportID: PropTypes.string.isRequired, + requestReportID: string; /** The ID of the current report */ - reportID: PropTypes.string.isRequired, + reportID: string; /** Is this IOUACTION the most recent? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, + isMostRecentIOUReportAction: boolean; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: refPropTypes, + contextMenuAnchor?: ContextMenuAnchor; /** Callback for updating context menu active state, used for showing context menu */ - checkIfContextMenuActive: PropTypes.func, - - /* Onyx Props */ - /** chatReport associated with iouReport */ - chatReport: reportPropTypes, - - /** IOU report data object */ - iouReport: iouReportPropTypes, - - /** Array of report actions for this report */ - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + checkIfContextMenuActive?: () => void; /** Whether the IOU is hovered so we can modify its style */ - isHovered: PropTypes.bool, - - network: networkPropTypes.isRequired, + isHovered?: boolean; /** Whether a message is a whisper */ - isWhisper: PropTypes.bool, + isWhisper?: boolean; /** Styles to be assigned to Container */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - contextMenuAnchor: undefined, - checkIfContextMenuActive: () => {}, - chatReport: {}, - iouReport: {}, - reportActions: {}, - isHovered: false, - style: [], - isWhisper: false, + style?: StyleProp; }; function MoneyRequestAction({ @@ -87,31 +70,32 @@ function MoneyRequestAction({ reportID, isMostRecentIOUReportAction, contextMenuAnchor, - checkIfContextMenuActive, + checkIfContextMenuActive = () => {}, chatReport, iouReport, reportActions, - isHovered, - network, + isHovered = false, style, - isWhisper, -}) { + isWhisper = false, +}: MoneyRequestActionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const isSplitBillAction = lodashGet(action, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; + const {isOffline} = useNetwork(); + + const isSplitBillAction = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; const onMoneyRequestPreviewPressed = () => { if (isSplitBillAction) { - const reportActionID = lodashGet(action, 'reportActionID', '0'); + const reportActionID = action.reportActionID ?? '0'; Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID)); return; } // If the childReportID is not present, we need to create a new thread - const childReportID = lodashGet(action, 'childReportID', 0); + const childReportID = action?.childReportID ?? '0'; if (!childReportID) { const thread = ReportUtils.buildTransactionThread(action, requestReportID); - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []); Report.openReport(thread.reportID, userLogins, thread, action.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; @@ -124,12 +108,12 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if ( - !_.isEmpty(iouReport) && - !_.isEmpty(reportActions) && - chatReport.iouReportID && + !isEmptyObject(iouReport) && + !isEmptyObject(reportActions) && + chatReport?.iouReportID && isMostRecentIOUReportAction && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && - network.isOffline + isOffline ) { shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } @@ -147,29 +131,24 @@ function MoneyRequestAction({ checkIfContextMenuActive={checkIfContextMenuActive} shouldShowPendingConversionMessage={shouldShowPendingConversionMessage} onPreviewPressed={onMoneyRequestPreviewPressed} - containerStyles={[styles.cursorPointer, isHovered ? styles.reportPreviewBoxHoverBorder : undefined, ...style]} + containerStyles={[styles.cursorPointer, isHovered ? styles.reportPreviewBoxHoverBorder : undefined, style]} isHovered={isHovered} isWhisper={isWhisper} /> ); } -MoneyRequestAction.propTypes = propTypes; -MoneyRequestAction.defaultProps = defaultProps; MoneyRequestAction.displayName = 'MoneyRequestAction'; -export default compose( - withOnyx({ - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({requestReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`, - }, - reportActions: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, - canEvict: false, - }, - }), - withNetwork(), -)(MoneyRequestAction); +export default withOnyx({ + chatReport: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, + }, + iouReport: { + key: ({requestReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${requestReportID}`, + }, + reportActions: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + canEvict: false, + }, +})(MoneyRequestAction); diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.tsx similarity index 63% rename from src/components/ReportActionItem/MoneyRequestPreview.js rename to src/components/ReportActionItem/MoneyRequestPreview.tsx index 13f75b9869a0..f321c63375d0 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.tsx @@ -1,22 +1,20 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import {truncate} from 'lodash'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import lodashSortBy from 'lodash/sortBy'; import React from 'react'; import {View} from 'react-native'; +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithFeedback from '@components/Pressable/PressableWithoutFeedback'; -import refPropTypes from '@components/refPropTypes'; import RenderHTML from '@components/RenderHTML'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; -import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -31,107 +29,99 @@ 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 walletTermsPropTypes from '@pages/EnablePayments/walletTermsPropTypes'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {IOUMessage} from '@src/types/onyx/OriginalMessage'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import ReportActionItemImages from './ReportActionItemImages'; -const propTypes = { +type MoneyRequestPreviewOnyxProps = { + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; + + /** Chat report associated with iouReport */ + chatReport: OnyxEntry; + + /** IOU report data object */ + iouReport: OnyxEntry; + + /** Session info for the currently logged in user. */ + session: OnyxEntry; + + /** The transaction attached to the action.message.iouTransactionID */ + transaction: OnyxEntry; + + /** Information about the user accepting the terms for payments */ + walletTerms: OnyxEntry; +}; + +type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { /** The active IOUReport, used for Onyx subscription */ + // The iouReportID is used inside withOnyx HOC // eslint-disable-next-line react/no-unused-prop-types - iouReportID: PropTypes.string.isRequired, + iouReportID: string; /** The associated chatReport */ - chatReportID: PropTypes.string.isRequired, + chatReportID: string; /** The ID of the current report */ - reportID: PropTypes.string.isRequired, + reportID: string; /** Callback for the preview pressed */ - onPreviewPressed: PropTypes.func, + onPreviewPressed: (event?: GestureResponderEvent | KeyboardEvent) => void; /** All the data of the action, used for showing context menu */ - action: PropTypes.shape(reportActionPropTypes), + action: OnyxTypes.ReportAction; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: refPropTypes, + contextMenuAnchor?: ContextMenuAnchor; /** Callback for updating context menu active state, used for showing context menu */ - checkIfContextMenuActive: PropTypes.func, + checkIfContextMenuActive?: () => void; /** Extra styles to pass to View wrapper */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /* Onyx Props */ - - /** chatReport associated with iouReport */ - chatReport: reportPropTypes, - - /** IOU report data object */ - iouReport: iouReportPropTypes, + containerStyles?: StyleProp; /** True if this is this IOU is a split instead of a 1:1 request */ - isBillSplit: PropTypes.bool.isRequired, + isBillSplit: boolean; /** True if the IOU Preview card is hovered */ - isHovered: PropTypes.bool, - - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf( - PropTypes.shape({ - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, - }), - ), - - /** The transaction attached to the action.message.iouTransactionID */ - transaction: transactionPropTypes, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user email */ - email: PropTypes.string, - }), - - /** Information about the user accepting the terms for payments */ - walletTerms: walletTermsPropTypes, + isHovered?: boolean; /** Whether or not an IOU report contains money requests in a different currency * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet */ - shouldShowPendingConversionMessage: PropTypes.bool, + shouldShowPendingConversionMessage?: boolean; /** Whether a message is a whisper */ - isWhisper: PropTypes.bool, + isWhisper?: boolean; }; -const defaultProps = { - iouReport: {}, - onPreviewPressed: null, - action: undefined, - contextMenuAnchor: undefined, - checkIfContextMenuActive: () => {}, - containerStyles: [], - walletTerms: {}, - chatReport: {}, - isHovered: false, - personalDetails: {}, - session: { - email: null, - }, - transaction: {}, - shouldShowPendingConversionMessage: false, - isWhisper: false, -}; - -function MoneyRequestPreview(props) { +function MoneyRequestPreview({ + iouReport, + isBillSplit, + session, + action, + personalDetails, + chatReport, + transaction, + contextMenuAnchor, + chatReportID, + reportID, + onPreviewPressed, + containerStyles, + walletTerms, + checkIfContextMenuActive = () => {}, + shouldShowPendingConversionMessage = false, + isHovered = false, + isWhisper = false, +}: MoneyRequestPreviewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -139,40 +129,40 @@ function MoneyRequestPreview(props) { const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const parser = new ExpensiMark(); - if (_.isEmpty(props.iouReport) && !props.isBillSplit) { + if (isEmptyObject(iouReport) && !isBillSplit) { return null; } - const sessionAccountID = lodashGet(props.session, 'accountID', null); - const managerID = props.iouReport.managerID || ''; - const ownerAccountID = props.iouReport.ownerAccountID || ''; - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.chatReport); + const sessionAccountID = session?.accountID; + const managerID = iouReport?.managerID ?? -1; + const ownerAccountID = iouReport?.ownerAccountID ?? -1; + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); - const participantAccountIDs = props.isBillSplit ? lodashGet(props.action, 'originalMessage.participantAccountIDs', []) : [managerID, ownerAccountID]; - const participantAvatars = OptionsListUtils.getAvatarsForAccountIDs(participantAccountIDs, props.personalDetails); - const sortedParticipantAvatars = _.sortBy(participantAvatars, (avatar) => avatar.id); - if (isPolicyExpenseChat && props.isBillSplit) { - sortedParticipantAvatars.push(ReportUtils.getWorkspaceIcon(props.chatReport)); + const participantAccountIDs = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && isBillSplit ? action.originalMessage.participantAccountIDs ?? [] : [managerID, ownerAccountID]; + const participantAvatars = OptionsListUtils.getAvatarsForAccountIDs(participantAccountIDs, personalDetails ?? {}); + const sortedParticipantAvatars = lodashSortBy(participantAvatars, (avatar) => avatar.id); + if (isPolicyExpenseChat && isBillSplit) { + sortedParticipantAvatars.push(ReportUtils.getWorkspaceIcon(chatReport)); } // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerID === sessionAccountID; - const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(props.transaction); + const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(transaction) ?? {}; const description = truncate(requestComment, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); - const hasReceipt = TransactionUtils.hasReceipt(props.transaction); - const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction); - const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction); - const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction); - const isCardTransaction = TransactionUtils.isCardTransaction(props.transaction); - const isSettled = ReportUtils.isSettled(props.iouReport.reportID); - const isDeleted = lodashGet(props.action, 'pendingAction', null) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const hasReceipt = TransactionUtils.hasReceipt(transaction); + const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); + const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); + const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); + const isCardTransaction = TransactionUtils.isCardTransaction(transaction); + const isSettled = ReportUtils.isSettled(iouReport?.reportID); + const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // Show the merchant for IOUs and expenses only if they are custom or not related to scanning smartscan - const shouldShowMerchant = !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; - const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; - const hasPendingWaypoints = lodashGet(props.transaction, 'pendingFields.waypoints', null); + const shouldShowMerchant = !!requestMerchant && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; + const shouldShowDescription = !!description && !shouldShowMerchant && !isScanning; + const hasPendingWaypoints = transaction?.pendingFields?.waypoints; let merchantOrDescription = requestMerchant; if (!shouldShowMerchant) { @@ -181,20 +171,20 @@ function MoneyRequestPreview(props) { merchantOrDescription = requestMerchant.replace(CONST.REGEX.FIRST_SPACE, translate('common.tbd')); } - const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction)] : []; + const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(transaction)] : []; - const getSettledMessage = () => { + const getSettledMessage = (): string => { if (isCardTransaction) { return translate('common.done'); } return translate('iou.settledExpensify'); }; - const showContextMenu = (event) => { - showContextMenuForReport(event, props.contextMenuAnchor, props.reportID, props.action, props.checkIfContextMenuActive); + const showContextMenu = (event: GestureResponderEvent) => { + showContextMenuForReport(event, contextMenuAnchor, reportID, action, checkIfContextMenuActive); }; - const getPreviewHeaderText = () => { + const getPreviewHeaderText = (): string => { if (isDistanceRequest) { return translate('common.distance'); } @@ -203,30 +193,30 @@ function MoneyRequestPreview(props) { return translate('common.receipt'); } - if (props.isBillSplit) { + if (isBillSplit) { return translate('iou.split'); } if (isCardTransaction) { let message = translate('iou.card'); - if (TransactionUtils.isPending(props.transaction)) { + if (TransactionUtils.isPending(transaction)) { message += ` • ${translate('iou.pending')}`; } return message; } let message = translate('iou.cash'); - if (ReportUtils.isPaidGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) { + if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { message += ` • ${translate('iou.approved')}`; - } else if (props.iouReport.isWaitingOnBankAccount) { + } else if (iouReport?.isWaitingOnBankAccount) { message += ` • ${translate('iou.pending')}`; - } else if (props.iouReport.isCancelledIOU) { + } else if (iouReport?.isCancelledIOU) { message += ` • ${translate('iou.canceled')}`; } return message; }; - const getDisplayAmountText = () => { + const getDisplayAmountText = (): string => { if (isDistanceRequest) { return requestAmount && !hasPendingWaypoints ? CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency) : translate('common.tbd'); } @@ -235,19 +225,16 @@ function MoneyRequestPreview(props) { return translate('iou.receiptScanning'); } - if (!isSettled && TransactionUtils.hasMissingSmartscanFields(props.transaction)) { + if (!isSettled && TransactionUtils.hasMissingSmartscanFields(transaction)) { return Localize.translateLocal('iou.receiptMissingDetails'); } return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); }; - const getDisplayDeleteAmountText = () => { - const {amount, currency} = ReportUtils.getTransactionDetails(props.action.originalMessage); - - if (isDistanceRequest) { - return CurrencyUtils.convertToDisplayString(TransactionUtils.getAmount(props.action.originalMessage), currency); - } + const getDisplayDeleteAmountText = (): string => { + const iouOriginalMessage: IOUMessage | EmptyObject = action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? action.originalMessage : {}; + const {amount = 0, currency = CONST.CURRENCY.USD} = iouOriginalMessage; return CurrencyUtils.convertToDisplayString(amount, currency); }; @@ -257,36 +244,34 @@ function MoneyRequestPreview(props) { const childContainer = ( { PaymentMethods.clearWalletTermsError(); - Report.clearIOUError(props.chatReportID); + Report.clearIOUError(chatReportID); }} errorRowStyles={[styles.mbn1]} needsOffscreenAlphaCompositing > {hasReceipt && ( )} - {_.isEmpty(props.transaction) && - !ReportActionsUtils.isMessageDeleted(props.action) && - lodashGet(props.action, 'pendingAction') !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( + {isEmptyObject(transaction) && !ReportActionsUtils.isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( ) : ( - {getPreviewHeaderText() + (isSettled && !props.iouReport.isCancelledIOU ? ` • ${getSettledMessage()}` : '')} + {getPreviewHeaderText() + (isSettled && !iouReport?.isCancelledIOU ? ` • ${getSettledMessage()}` : '')} {!isSettled && hasFieldErrors && ( {displayAmount} - {ReportUtils.isSettled(props.iouReport.reportID) && !props.isBillSplit && ( + {ReportUtils.isSettled(iouReport?.reportID) && !isBillSplit && ( )} - {props.isBillSplit && ( + {isBillSplit && ( @@ -331,17 +315,17 @@ function MoneyRequestPreview(props) { - {!isCurrentUserManager && props.shouldShowPendingConversionMessage && ( + {!isCurrentUserManager && shouldShowPendingConversionMessage && ( {translate('iou.pendingConversionMessage')} )} {shouldShowDescription && } {shouldShowMerchant && {merchantOrDescription}} - {props.isBillSplit && !_.isEmpty(participantAccountIDs) && requestAmount > 0 && ( + {isBillSplit && participantAccountIDs.length > 0 && requestAmount && requestAmount > 0 && ( {translate('iou.amountEach', { amount: CurrencyUtils.convertToDisplayString( - IOUUtils.calculateAmount(isPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount, requestCurrency), + IOUUtils.calculateAmount(isPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount, requestCurrency ?? ''), requestCurrency, ), })} @@ -355,32 +339,30 @@ function MoneyRequestPreview(props) { ); - if (!props.onPreviewPressed) { + if (!onPreviewPressed) { return childContainer; } - const shouldDisableOnPress = props.isBillSplit && _.isEmpty(props.transaction); + const shouldDisableOnPress = isBillSplit && isEmptyObject(transaction); return ( DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={showContextMenu} - accessibilityLabel={props.isBillSplit ? translate('iou.split') : translate('iou.cash')} + accessibilityLabel={isBillSplit ? translate('iou.split') : translate('iou.cash')} accessibilityHint={CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency)} - style={[styles.moneyRequestPreviewBox, ...props.containerStyles, shouldDisableOnPress && styles.cursorDefault]} + style={[styles.moneyRequestPreviewBox, containerStyles, shouldDisableOnPress && styles.cursorDefault]} > {childContainer} ); } -MoneyRequestPreview.propTypes = propTypes; -MoneyRequestPreview.defaultProps = defaultProps; MoneyRequestPreview.displayName = 'MoneyRequestPreview'; -export default withOnyx({ +export default withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, @@ -394,7 +376,10 @@ export default withOnyx({ key: ONYXKEYS.SESSION, }, transaction: { - key: ({action}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${(action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0}`, + key: ({action}) => { + const originalMessage = action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? action.originalMessage : undefined; + return `${ONYXKEYS.COLLECTION.TRANSACTION}${originalMessage?.IOUTransactionID ?? 0}`; + }, }, walletTerms: { key: ONYXKEYS.WALLET_TERMS, diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.tsx similarity index 71% rename from src/components/ReportActionItem/MoneyRequestView.js rename to src/components/ReportActionItem/MoneyRequestView.tsx index 5632d3c22e7d..3533506797bb 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -1,30 +1,24 @@ -import lodashGet from 'lodash/get'; -import lodashValues from 'lodash/values'; -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import categoryPropTypes from '@components/categoryPropTypes'; +import type {OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import SpacerView from '@components/SpacerView'; import Switch from '@components/Switch'; -import tagPropTypes from '@components/tagPropTypes'; import Text from '@components/Text'; -import transactionPropTypes from '@components/transactionPropTypes'; import ViolationMessages from '@components/ViolationMessages'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useViolations from '@hooks/useViolations'; +import type {ViolationField} from '@hooks/useViolations'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CardUtils from '@libs/CardUtils'; -import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -34,97 +28,68 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; -import {policyDefaultProps, policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import * as Transaction from '@userActions/Transaction'; 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 {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction'; import ReportActionItemImage from './ReportActionItemImage'; -const violationNames = lodashValues(CONST.VIOLATIONS); +type MoneyRequestViewTransactionOnyxProps = { + /** The transaction associated with the transactionThread */ + transaction: OnyxEntry; -const transactionViolationPropType = PropTypes.shape({ - type: PropTypes.string.isRequired, - name: PropTypes.oneOf(violationNames).isRequired, - data: PropTypes.shape({ - rejectedBy: PropTypes.string, - rejectReason: PropTypes.string, - amount: PropTypes.string, - surcharge: PropTypes.number, - invoiceMarkup: PropTypes.number, - maxAge: PropTypes.number, - tagName: PropTypes.string, - formattedLimitAmount: PropTypes.string, - categoryLimit: PropTypes.string, - limit: PropTypes.string, - category: PropTypes.string, - brokenBankConnection: PropTypes.bool, - isAdmin: PropTypes.bool, - email: PropTypes.string, - isTransactionOlderThan7Days: PropTypes.bool, - member: PropTypes.string, - taxName: PropTypes.string, - }), -}); + /** Violations detected in this transaction */ + transactionViolations: OnyxEntry; +}; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes.isRequired, +type MoneyRequestViewOnyxPropsWithoutTransaction = { + /** The policy object for the current route */ + policy: OnyxEntry; - /** Whether we should display the horizontal rule below the component */ - shouldShowHorizontalRule: PropTypes.bool.isRequired, + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; + + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; - /* Onyx Props */ /** The expense report or iou report (only will have a value if this is a transaction thread) */ - parentReport: iouReportPropTypes, + parentReport: OnyxEntry; /** The actions from the parent report */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), - - /** The policy the report is tied to */ - ...policyPropTypes, - - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), - - /** The transaction associated with the transactionThread */ - transaction: transactionPropTypes, - - /** Violations detected in this transaction */ - transactionViolations: PropTypes.arrayOf(transactionViolationPropType), + parentReportActions: OnyxEntry; +}; - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, +type MoneyRequestViewPropsWithoutTransaction = MoneyRequestViewOnyxPropsWithoutTransaction & { + /** The report currently being looked at */ + report: OnyxTypes.Report; - ...withCurrentUserPersonalDetailsPropTypes, + /** Whether we should display the horizontal rule below the component */ + shouldShowHorizontalRule: boolean; }; -const defaultProps = { - parentReport: {}, - parentReportActions: {}, - transaction: { - amount: 0, - currency: CONST.CURRENCY.USD, - comment: {comment: ''}, - }, - transactionViolations: [], - policyCategories: {}, - policyTags: {}, - ...policyDefaultProps, -}; +type MoneyRequestViewProps = MoneyRequestViewTransactionOnyxProps & MoneyRequestViewPropsWithoutTransaction; -function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy, transactionViolations}) { +function MoneyRequestView({ + report, + parentReport, + parentReportActions, + policyCategories, + shouldShowHorizontalRule, + transaction, + policyTags, + policy, + transactionViolations, +}: MoneyRequestViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); - const parentReportAction = parentReportActions[report.parentReportActionID] || {}; + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; const moneyRequestReport = parentReport; const { created: transactionDate, @@ -138,20 +103,20 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate originalAmount: transactionOriginalAmount, originalCurrency: transactionOriginalCurrency, cardID: transactionCardID, - } = ReportUtils.getTransactionDetails(transaction); + } = ReportUtils.getTransactionDetails(transaction) ?? {}; const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); let formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; - const hasPendingWaypoints = lodashGet(transaction, 'pendingFields.waypoints', null); + const hasPendingWaypoints = transaction?.pendingFields?.waypoints; if (isDistanceRequest && (!formattedTransactionAmount || hasPendingWaypoints)) { formattedTransactionAmount = translate('common.tbd'); } const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); - const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; + const cardProgramName = isCardTransaction && transactionCardID !== undefined ? CardUtils.getCardDescription(transactionCardID) : ''; // Flags for allowing or disallowing editing a money request - const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); + const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); const isCancelled = moneyRequestReport && moneyRequestReport.isCancelledIOU; // Used for non-restricted fields such as: description, category, tag, billable, etc. @@ -168,26 +133,30 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate // Fetches only the first tag, for now const policyTag = PolicyUtils.getTag(policyTags); - const policyTagsList = lodashGet(policyTag, 'tags', {}); + const policyTagsList = policyTag?.tags ?? {}; // Flags for showing categories and tags - const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories))); - const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList))); - const shouldShowBillable = isPolicyExpenseChat && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true)); + // transactionCategory can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + // transactionTag can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(Object.values(policyTagsList))); + const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true)); - const {getViolationsForField} = useViolations(transactionViolations); - const hasViolations = useCallback((field) => canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); + const {getViolationsForField} = useViolations(transactionViolations ?? []); + const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); let amountDescription = `${translate('iou.amount')}`; const saveBillable = useCallback( - (newBillable) => { + (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal if (newBillable === TransactionUtils.getBillable(transaction)) { Navigation.dismissModal(); return; } - IOU.updateMoneyRequestBillable(transaction.transactionID, report.reportID, newBillable); + IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '', report?.reportID, newBillable); Navigation.dismissModal(); }, [transaction, report], @@ -226,8 +195,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); } - const pendingAction = lodashGet(transaction, 'pendingAction'); - const getPendingFieldAction = (fieldPath) => lodashGet(transaction, fieldPath) || pendingAction; + const pendingAction = transaction?.pendingAction; + const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => transaction?.pendingFields?.[fieldPath] ?? pendingAction; return ( @@ -236,18 +205,22 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate {hasReceipt && ( { + if (!transaction?.transactionID) { + return; + } + Transaction.clearError(transaction.transactionID); }} > )} {canUseViolations && } - + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))} - brickRoadIndicator={hasViolations('amount') || (hasErrors && transactionAmount === 0) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={hasViolations('amount') || (hasErrors && transactionAmount === 0) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''} /> {canUseViolations && } - + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} - brickRoadIndicator={hasViolations('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={hasViolations('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} numberOfLinesTitle={0} /> {canUseViolations && } {isDistanceRequest ? ( - + ) : ( - + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))} - brickRoadIndicator={hasViolations('merchant') || (hasErrors && isEmptyMerchant && isPolicyExpenseChat) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={hasViolations('merchant') || (hasErrors && isEmptyMerchant && isPolicyExpenseChat) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={hasErrors && isPolicyExpenseChat && isEmptyMerchant ? translate('common.error.enterMerchant') : ''} /> {canUseViolations && } )} - + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))} - brickRoadIndicator={hasViolations('date') || (hasErrors && transactionDate === '') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={hasViolations('date') || (hasErrors && transactionDate === '') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} error={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''} /> {canUseViolations && } {shouldShowCategory && ( - + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.CATEGORY))} - brickRoadIndicator={hasViolations('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={hasViolations('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> {canUseViolations && } )} {shouldShowTag && ( - + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAG))} - brickRoadIndicator={hasViolations('tag') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={hasViolations('tag') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> {canUseViolations && } )} {isCardTransaction && ( - + {translate('common.billable')} @@ -406,50 +379,42 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate ); } -MoneyRequestView.propTypes = propTypes; -MoneyRequestView.defaultProps = defaultProps; MoneyRequestView.displayName = 'MoneyRequestView'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report.policyID}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`, - }, - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, - canEvict: false, - }, - }), - withOnyx({ +export default withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, + }, + policyCategories: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report.policyID}`, + }, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`, + }, + parentReport: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, + canEvict: false, + }, +})( + withOnyx({ transaction: { key: ({report, parentReportActions}) => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0); + 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}${transactionID}`; }, }, transactionViolations: { key: ({report}) => { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0); + const originalMessage = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage : undefined; + const transactionID = originalMessage?.IOUTransactionID ?? 0; return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; }, }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`, - }, - }), -)(MoneyRequestView); + })(MoneyRequestView), +); diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index d1fc72767f56..04a99e00c6bf 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type {ReactElement} from 'react'; import type {ImageSourcePropType, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import EReceiptThumbnail from '@components/EReceiptThumbnail'; import Image from '@components/Image'; @@ -21,13 +22,13 @@ type ReportActionItemImageProps = { thumbnail?: string | ImageSourcePropType | null; /** URI for the image or local numeric reference for the image */ - image: string | ImageSourcePropType; + image?: string | ImageSourcePropType; /** whether or not to enable the image preview modal */ enablePreviewModal?: boolean; /* The transaction associated with this image, if any. Passed for handling eReceipts. */ - transaction?: Transaction; + transaction?: OnyxEntry; /** whether thumbnail is refer the local file or not */ isLocalFile?: boolean; diff --git a/src/languages/types.ts b/src/languages/types.ts index 11adf01ac252..d4ec48eb3b41 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -113,7 +113,7 @@ type SplitAmountParams = {amount: number}; type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; -type AmountEachParams = {amount: number}; +type AmountEachParams = {amount: string}; type PayerOwesAmountParams = {payer: string; amount: number | string}; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index bc40f93dd13b..46ca550eaa1a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -7,6 +7,7 @@ import CONST from '@src/CONST'; import translations from '@src/languages/translations'; import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReceiptError} from '@src/types/onyx/Transaction'; import LocaleListener from './LocaleListener'; import BaseLocaleListener from './LocaleListener/BaseLocaleListener'; @@ -102,7 +103,10 @@ type MaybePhraseKey = string | [string, Record & {isTranslated? /** * Return translated string for given error. */ -function translateIfPhraseKey(message: MaybePhraseKey): string { +function translateIfPhraseKey(message: MaybePhraseKey): string; +function translateIfPhraseKey(message: ReceiptError): ReceiptError; +function translateIfPhraseKey(message: MaybePhraseKey | ReceiptError): string | ReceiptError; +function translateIfPhraseKey(message: MaybePhraseKey | ReceiptError): string | ReceiptError { if (!message || (Array.isArray(message) && message.length === 0)) { return ''; } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ee553d6c53ab..8cbe5bfa2d23 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -10,7 +10,21 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import type { + Beta, + Login, + PersonalDetails, + PersonalDetailsList, + Policy, + PolicyCategories, + PolicyCategory, + PolicyTag, + Report, + ReportAction, + ReportActions, + Transaction, + TransactionViolation, +} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; @@ -781,8 +795,8 @@ function getEnabledCategoriesCount(options: PolicyCategories): number { /** * Verifies that there is at least one enabled option */ -function hasEnabledOptions(options: PolicyCategories): boolean { - return Object.values(options).some((option) => option.enabled); +function hasEnabledOptions(options: PolicyCategory[] | PolicyTag[]): boolean { + return options.some((option) => option.enabled); } /** diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 86f8bb72272d..948d8094d551 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,6 +1,7 @@ import Str from 'expensify-common/lib/str'; import _ from 'lodash'; import type {ImageSourcePropType} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import ReceiptDoc from '@assets/images/receipt-doc.png'; import ReceiptGeneric from '@assets/images/receipt-generic.png'; import ReceiptHTML from '@assets/images/receipt-html.png'; @@ -31,14 +32,13 @@ type FileNameAndExtension = { * @param receiptPath * @param receiptFileName */ -function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { +function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { if (Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; } - // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg // If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one - const errors = _.findLast(transaction.errors) as ReceiptError | undefined; + const errors = _.findLast(transaction?.errors) as ReceiptError | undefined; const path = errors?.source ?? transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? ''; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 68085c8d1255..8e48eddea0ac 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -266,7 +266,7 @@ function getDescription(transaction: OnyxEntry): string { /** * Return the amount field from the transaction, return the modifiedAmount if present. */ -function getAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number { +function getAmount(transaction: OnyxEntry, isFromExpenseReport?: boolean): number { // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value if (!isFromExpenseReport) { const amount = transaction?.modifiedAmount ?? 0; @@ -393,8 +393,8 @@ function getHeaderTitleTranslationKey(transaction: Transaction): string { /** * Determine whether a transaction is made with an Expensify card. */ -function isExpensifyCardTransaction(transaction: Transaction): boolean { - if (!transaction.cardID) { +function isExpensifyCardTransaction(transaction: OnyxEntry): boolean { + if (!transaction?.cardID) { return false; } return isExpensifyCard(transaction.cardID); @@ -403,7 +403,7 @@ function isExpensifyCardTransaction(transaction: Transaction): boolean { /** * Determine whether a transaction is made with a card (Expensify or Company Card). */ -function isCardTransaction(transaction: Transaction): boolean { +function isCardTransaction(transaction: OnyxEntry): boolean { const cardID = transaction?.cardID ?? 0; return isCorporateCard(cardID); } @@ -411,8 +411,8 @@ function isCardTransaction(transaction: Transaction): boolean { /** * Check if the transaction status is set to Pending. */ -function isPending(transaction: Transaction): boolean { - if (!transaction.status) { +function isPending(transaction: OnyxEntry): boolean { + if (!transaction?.status) { return false; } return transaction.status === CONST.TRANSACTION.STATUS.PENDING; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f2bdb097497e..4db89a1e926b 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1175,8 +1175,8 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { * Updates the billable field of a money request * * @param {String} transactionID - * @param {Number} transactionThreadReportID - * @param {String} val + * @param {String} transactionThreadReportID + * @param {Boolean} val */ function updateMoneyRequestBillable(transactionID, transactionThreadReportID, val) { const transactionChanges = { diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 58a21dcf4df5..c14ede73b622 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -8,6 +8,9 @@ type PolicyTag = { /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ // eslint-disable-next-line @typescript-eslint/naming-convention 'GL Code': string; + + /** Nested tags */ + tags: PolicyTags; }; type PolicyTags = Record; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index dfdfad703991..fe7ca7436a81 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -49,6 +49,8 @@ type Route = { type Routes = Record; +type TransactionPendingFieldsKey = keyof Transaction | keyof Comment; + type ReceiptError = {error?: string; source: string; filename: string}; type ReceiptErrors = Record; @@ -80,7 +82,7 @@ type Transaction = { routes?: Routes; transactionID: string; tag: string; - pendingFields?: Partial<{[K in keyof Transaction | keyof Comment]: ValueOf}>; + pendingFields?: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}>; /** Card Transactions */ @@ -101,4 +103,4 @@ type Transaction = { }; export default Transaction; -export type {WaypointCollection, Comment, Receipt, Waypoint, ReceiptError}; +export type {WaypointCollection, Comment, Receipt, Waypoint, ReceiptError, ReceiptErrors, TransactionPendingFieldsKey};