diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index daf02a6f48a8..ea915c25a6ab 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -463,7 +463,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; - [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; + [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolation[]; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index b0959d43aa96..2e6f36d5cc07 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -12,7 +12,7 @@ type DisplayNameWithTooltip = { login?: string; /** The avatar for the tooltip fallback */ - avatar: AvatarSource; + avatar?: AvatarSource; }; type DisplayNamesProps = { diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js deleted file mode 100644 index 4c8b0e1102b9..000000000000 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ /dev/null @@ -1,222 +0,0 @@ -import {FlashList} from '@shopify/flash-list'; -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 participantPropTypes from '@components/participantPropTypes'; -import transactionPropTypes from '@components/transactionPropTypes'; -import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID'; -import usePermissions from '@hooks/usePermissions'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import {transactionViolationsPropType} from '@libs/Violations/propTypes'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; -import stylePropTypes from '@styles/stylePropTypes'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import OptionRowLHNData from './OptionRowLHNData'; - -const propTypes = { - /** Wrapper style for the section list */ - style: stylePropTypes, - - /** Extra styles for the section list container */ - contentContainerStyles: stylePropTypes.isRequired, - - /** Sections for the section list */ - data: PropTypes.arrayOf(PropTypes.string).isRequired, - - /** Callback to fire when a row is selected */ - onSelectRow: PropTypes.func.isRequired, - - /** Toggle between compact and default view of the option */ - optionMode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)).isRequired, - - /** Whether to allow option focus or not */ - shouldDisableFocusOptions: PropTypes.bool, - - /** The policy which the user has access to and which the report could be tied to */ - policy: PropTypes.shape({ - /** The ID of the policy */ - id: PropTypes.string, - /** Name of the policy */ - name: PropTypes.string, - /** Avatar of the policy */ - avatar: PropTypes.string, - }), - - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - - /** Array of report actions for this report */ - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), - - /** Indicates which locale the user currently has selected */ - preferredLocale: PropTypes.string, - - /** List of users' personal details */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - /** The transaction from the parent report action */ - transactions: PropTypes.objectOf(transactionPropTypes), - - /** List of draft comments */ - draftComments: PropTypes.objectOf(PropTypes.string), - - /** The list of transaction violations */ - transactionViolations: transactionViolationsPropType, - - ...withCurrentReportIDPropTypes, -}; - -const defaultProps = { - style: undefined, - shouldDisableFocusOptions: false, - reportActions: {}, - reports: {}, - policy: {}, - preferredLocale: CONST.LOCALES.DEFAULT, - personalDetails: {}, - transactions: {}, - draftComments: {}, - transactionViolations: {}, - ...withCurrentReportIDDefaultProps, -}; - -const keyExtractor = (item) => `report_${item}`; - -function LHNOptionsList({ - style, - contentContainerStyles, - data, - onSelectRow, - optionMode, - shouldDisableFocusOptions, - reports, - reportActions, - policy, - preferredLocale, - personalDetails, - transactions, - draftComments, - currentReportID, - transactionViolations, -}) { - const styles = useThemeStyles(); - const {canUseViolations} = usePermissions(); - /** - * Function which renders a row in the list - * - * @param {Object} params - * @param {Object} params.item - * - * @return {Component} - */ - const renderItem = useCallback( - ({item: reportID}) => { - const itemFullReport = reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; - const itemReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - const itemParentReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport.parentReportID}`] || {}; - const itemParentReportAction = itemParentReportActions[itemFullReport.parentReportActionID] || {}; - const itemPolicy = policy[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport.policyID}`] || {}; - const transactionID = lodashGet(itemParentReportAction, ['originalMessage', 'IOUTransactionID'], ''); - const itemTransaction = transactionID ? transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {}; - const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || ''; - const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport.ownerAccountID, itemParentReportAction.actorAccountID]; - - const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); - - return ( - - ); - }, - [ - currentReportID, - draftComments, - onSelectRow, - optionMode, - personalDetails, - policy, - preferredLocale, - reportActions, - reports, - shouldDisableFocusOptions, - transactions, - transactionViolations, - canUseViolations, - ], - ); - - return ( - - - - ); -} - -LHNOptionsList.propTypes = propTypes; -LHNOptionsList.defaultProps = defaultProps; -LHNOptionsList.displayName = 'LHNOptionsList'; - -export default compose( - withCurrentReportID, - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, - }), -)(LHNOptionsList); diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx new file mode 100644 index 000000000000..ecf320807b48 --- /dev/null +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -0,0 +1,141 @@ +import {FlashList} from '@shopify/flash-list'; +import type {ReactElement} from 'react'; +import React, {useCallback} from 'react'; +import {StyleSheet, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import withCurrentReportID from '@components/withCurrentReportID'; +import usePermissions from '@hooks/usePermissions'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import OptionRowLHNData from './OptionRowLHNData'; +import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types'; + +const keyExtractor = (item: string) => `report_${item}`; + +function LHNOptionsList({ + style, + contentContainerStyles, + data, + onSelectRow, + optionMode, + shouldDisableFocusOptions = false, + reports = {}, + reportActions = {}, + policy = {}, + preferredLocale = CONST.LOCALES.DEFAULT, + personalDetails = {}, + transactions = {}, + currentReportID = '', + draftComments = {}, + transactionViolations = {}, +}: LHNOptionsListProps) { + const styles = useThemeStyles(); + const {canUseViolations} = usePermissions(); + /** + * Function which renders a row in the list + */ + const renderItem = useCallback( + ({item: reportID}: RenderItemProps): ReactElement => { + const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; + const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; + const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; + const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport?.ownerAccountID, itemParentReportAction?.actorAccountID]; + const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); + + return ( + + ); + }, + [ + currentReportID, + draftComments, + onSelectRow, + optionMode, + personalDetails, + policy, + preferredLocale, + reportActions, + reports, + shouldDisableFocusOptions, + transactions, + transactionViolations, + canUseViolations, + ], + ); + + return ( + + + + ); +} + +LHNOptionsList.displayName = 'LHNOptionsList'; + +export default withCurrentReportID( + withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + reportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, + policy: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, + draftComments: { + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, + })(LHNOptionsList), +); + +export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.tsx similarity index 68% rename from src/components/LHNOptionsList/OptionRowLHN.js rename to src/components/LHNOptionsList/OptionRowLHN.tsx index fc4f05eefd22..1932cf6c6b7f 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,9 +1,7 @@ import {useFocusEffect} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useRef, useState} from 'react'; +import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; -import _ from 'underscore'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -27,51 +25,18 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {OptionRowLHNProps} from './types'; -const propTypes = { - /** Style for hovered state */ - // eslint-disable-next-line react/forbid-prop-types - hoverStyle: PropTypes.object, - - /** The ID of the report that the option is for */ - reportID: PropTypes.string.isRequired, - - /** Whether this option is currently in focus so we can modify its style */ - isFocused: PropTypes.bool, - - /** A function that is called when an option is selected. Selected option is passed as a param */ - onSelectRow: PropTypes.func, - - /** Toggle between compact and default view */ - viewMode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)), - - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** The item that should be rendered */ - // eslint-disable-next-line react/forbid-prop-types - optionItem: PropTypes.object, -}; - -const defaultProps = { - hoverStyle: undefined, - viewMode: 'default', - onSelectRow: () => {}, - style: null, - optionItem: null, - isFocused: false, -}; - -function OptionRowLHN(props) { +function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style}: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); + const popoverAnchor = useRef(null); const StyleUtils = useStyleUtils(); - const popoverAnchor = useRef(null); const isFocusedRef = useRef(true); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); - - const optionItem = props.optionItem; const [isContextMenuActive, setIsContextMenuActive] = useState(false); useFocusEffect( @@ -87,42 +52,37 @@ function OptionRowLHN(props) { return null; } - const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - if (isHidden && !props.isFocused && !optionItem.isPinned) { + const isHidden = optionItem?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + if (isHidden && !isFocused && !optionItem?.isPinned) { return null; } - const textStyle = props.isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textUnreadStyle = optionItem.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; - const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], props.style); - const alternateTextStyle = StyleUtils.combineStyles( - props.viewMode === CONST.OPTION_MODE.COMPACT - ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2] - : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting], - props.style, - ); + const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textUnreadStyle = optionItem?.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; + const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style]; + const alternateTextStyle = + viewMode === CONST.OPTION_MODE.COMPACT + ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] + : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style]; + const contentContainerStyles = - props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; - const sidebarInnerRowStyle = StyleSheet.flatten( - props.viewMode === CONST.OPTION_MODE.COMPACT + viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; + const sidebarInnerRowStyle = StyleSheet.flatten( + viewMode === CONST.OPTION_MODE.COMPACT ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); - const hoveredBackgroundColor = - (props.hoverStyle || styles.sidebarLinkHover) && (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - ? (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - : theme.sidebar; + const hoveredBackgroundColor = !!styles.sidebarLinkHover && 'backgroundColor' in styles.sidebarLinkHover ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction); - /** * Show the ReportActionContextMenu modal popover. * - * @param {Object} [event] - A press event. + * @param [event] - A press event. */ - const showPopover = (event) => { + const showPopover = (event: MouseEvent | GestureResponderEvent) => { if (!isFocusedRef.current && isSmallScreenWidth) { return; } @@ -131,33 +91,32 @@ function OptionRowLHN(props) { CONST.CONTEXT_MENU_TYPES.REPORT, event, '', - popoverAnchor, - props.reportID, + popoverAnchor.current, + reportID, '0', - props.reportID, + reportID, undefined, () => {}, () => setIsContextMenuActive(false), false, false, optionItem.isPinned, - optionItem.isUnread, + !!optionItem.isUnread, ); }; - const emojiCode = lodashGet(optionItem, 'status.emojiCode', ''); - const statusText = lodashGet(optionItem, 'status.text', ''); - const statusClearAfterDate = lodashGet(optionItem, 'status.clearAfter', ''); + const emojiCode = optionItem.status?.emojiCode ?? ''; + const statusText = optionItem.status?.text ?? ''; + const statusClearAfterDate = optionItem.status?.clearAfter ?? ''; const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText; - const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(ReportUtils.getReport(optionItem.reportID)); + const report = ReportUtils.getReport(optionItem.reportID ?? ''); + const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null); - const isGroupChat = - optionItem.type === CONST.REPORT.TYPE.CHAT && _.isEmpty(optionItem.chatType) && !optionItem.isThread && lodashGet(optionItem, 'displayNamesWithTooltips.length', 0) > 2; - const fullTitle = isGroupChat ? getGroupChatName(ReportUtils.getReport(optionItem.reportID)) : optionItem.text; - - const subscriptAvatarBorderColor = props.isFocused ? focusedBackgroundColor : theme.sidebar; + const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2; + const fullTitle = isGroupChat ? getGroupChatName(!isEmptyObject(report) ? report : null) : optionItem.text; + const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; return ( ( { - if (e) { - e.preventDefault(); - } + onPress={(event) => { + event?.preventDefault(); // Enable Composer to focus on clicking the same chat after opening the context menu. ReportActionComposeFocusManager.focus(); - props.onSelectRow(optionItem, popoverAnchor); + onSelectRow(optionItem, popoverAnchor); }} - onMouseDown={(e) => { + onMouseDown={(event) => { // Allow composer blur on right click - if (!e) { + if (!event) { return; } // Prevent composer blur on left click - e.preventDefault(); + event.preventDefault(); }} testID={optionItem.reportID} - onSecondaryInteraction={(e) => { - showPopover(e); + onSecondaryInteraction={(event) => { + showPopover(event); // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time if (DomUtils.getActiveElement()) { - DomUtils.getActiveElement().blur(); + (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); } }} withoutFocusOnSecondaryInteraction @@ -203,32 +160,32 @@ function OptionRowLHN(props) { styles.sidebarLink, styles.sidebarLinkInnerLHN, StyleUtils.getBackgroundColorStyle(theme.sidebar), - props.isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle || styles.sidebarLinkHover : null, + isFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, ]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - needsOffscreenAlphaCompositing={props.optionItem.icons.length >= 2} + needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} > - {!_.isEmpty(optionItem.icons) && + {(optionItem.icons?.length ?? 0) > 0 && (optionItem.shouldShowSubscript ? ( ) : ( @@ -237,13 +194,17 @@ function OptionRowLHN(props) { {isStatusVisible && ( @@ -265,7 +226,7 @@ function OptionRowLHN(props) { ) : null} - {optionItem.descriptiveText ? ( + {optionItem?.descriptiveText ? ( {optionItem.descriptiveText} @@ -324,10 +285,6 @@ function OptionRowLHN(props) { ); } -OptionRowLHN.propTypes = propTypes; -OptionRowLHN.defaultProps = defaultProps; OptionRowLHN.displayName = 'OptionRowLHN'; export default React.memo(OptionRowLHN); - -export {propTypes, defaultProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.tsx similarity index 56% rename from src/components/LHNOptionsList/OptionRowLHNData.js rename to src/components/LHNOptionsList/OptionRowLHNData.tsx index 8bdf065a94fd..dca74e880169 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,65 +1,14 @@ import {deepEqual} from 'fast-equals'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useRef} from 'react'; -import _ from 'underscore'; -import participantPropTypes from '@components/participantPropTypes'; -import transactionPropTypes from '@components/transactionPropTypes'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import {transactionViolationsPropType} from '@libs/Violations/propTypes'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; -import OptionRowLHN, {defaultProps as baseDefaultProps, propTypes as basePropTypes} from './OptionRowLHN'; - -const propTypes = { - /** Whether row should be focused */ - isFocused: PropTypes.bool, - - /** List of users' personal details */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - /** The preferred language for the app */ - preferredLocale: PropTypes.string, - - /** The full data of the report */ - // eslint-disable-next-line react/forbid-prop-types - fullReport: PropTypes.object, - - /** The policy which the user has access to and which the report could be tied to */ - policy: PropTypes.shape({ - /** The ID of the policy */ - id: PropTypes.string, - /** Name of the policy */ - name: PropTypes.string, - /** Avatar of the policy */ - avatar: PropTypes.string, - }), - - /** The action from the parent report */ - parentReportAction: PropTypes.shape(reportActionPropTypes), - - /** The transaction from the parent report action */ - transaction: transactionPropTypes, - - /** Any violations associated with the transaction */ - transactionViolations: transactionViolationsPropType, - - ...basePropTypes, -}; - -const defaultProps = { - isFocused: false, - personalDetails: {}, - fullReport: {}, - policy: {}, - parentReportAction: {}, - transaction: {}, - preferredLocale: CONST.LOCALES.DEFAULT, - ...baseDefaultProps, -}; +import type {OptionData} from '@src/libs/ReportUtils'; +import OptionRowLHN from './OptionRowLHN'; +import type {OptionRowLHNDataProps} from './types'; /* * This component gets the data from onyx for the actual @@ -68,11 +17,11 @@ const defaultProps = { * re-render if the data really changed. */ function OptionRowLHNData({ - isFocused, + isFocused = false, fullReport, reportActions, - personalDetails, - preferredLocale, + personalDetails = {}, + preferredLocale = CONST.LOCALES.DEFAULT, comment, policy, receiptTransactions, @@ -81,18 +30,18 @@ function OptionRowLHNData({ transactionViolations, canUseViolations, ...propsToForward -}) { +}: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; - const optionItemRef = useRef(); + const optionItemRef = useRef(); const linkedTransaction = useMemo(() => { const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); - const lastReportAction = _.first(sortedReportActions); + const lastReportAction = sortedReportActions[0]; return TransactionUtils.getLinkedTransaction(lastReportAction); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport.reportID, receiptTransactions, reportActions]); + }, [fullReport?.reportID, receiptTransactions, reportActions]); - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction); + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null); const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! @@ -100,15 +49,17 @@ function OptionRowLHNData({ report: fullReport, reportActions, personalDetails, - preferredLocale, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, policy, parentReportAction, - hasViolations, + hasViolations: !!hasViolations, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } + optionItemRef.current = item; + return item; // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed @@ -116,7 +67,7 @@ function OptionRowLHNData({ }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction, transactionViolations, canUseViolations]); useEffect(() => { - if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { + if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { return; } Report.setReportWithDraft(reportID, true); @@ -133,8 +84,6 @@ function OptionRowLHNData({ ); } -OptionRowLHNData.propTypes = propTypes; -OptionRowLHNData.defaultProps = defaultProps; OptionRowLHNData.displayName = 'OptionRowLHNData'; /** diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts new file mode 100644 index 000000000000..24cebb8e3da2 --- /dev/null +++ b/src/components/LHNOptionsList/types.ts @@ -0,0 +1,124 @@ +import type {ContentStyle} from '@shopify/flash-list'; +import type {RefObject} from 'react'; +import type {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; +import type CONST from '@src/CONST'; +import type {OptionData} from '@src/libs/ReportUtils'; +import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; + +type OptionMode = ValueOf; + +type LHNOptionsListOnyxProps = { + /** The policy which the user has access to and which the report could be tied to */ + policy: OnyxCollection; + + /** All reports shared with the user */ + reports: OnyxCollection; + + /** Array of report actions for this report */ + reportActions: OnyxCollection; + + /** Indicates which locale the user currently has selected */ + preferredLocale: OnyxEntry; + + /** List of users' personal details */ + personalDetails: OnyxEntry; + + /** The transaction from the parent report action */ + transactions: OnyxCollection; + + /** List of draft comments */ + draftComments: OnyxCollection; + + /** The list of transaction violations */ + transactionViolations: OnyxCollection; +}; + +type CustomLHNOptionsListProps = { + /** Wrapper style for the section list */ + style?: StyleProp; + + /** Extra styles for the section list container */ + contentContainerStyles?: StyleProp; + + /** Sections for the section list */ + data: string[]; + + /** Callback to fire when a row is selected */ + onSelectRow: (reportID: string) => void; + + /** Toggle between compact and default view of the option */ + optionMode: OptionMode; + + /** Whether to allow option focus or not */ + shouldDisableFocusOptions?: boolean; +}; + +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; + +type OptionRowLHNDataProps = { + /** Whether row should be focused */ + isFocused?: boolean; + + /** List of users' personal details */ + personalDetails?: PersonalDetailsList; + + /** The preferred language for the app */ + preferredLocale?: OnyxEntry; + + /** The full data of the report */ + fullReport: OnyxEntry; + + /** The policy which the user has access to and which the report could be tied to */ + policy?: OnyxEntry; + + /** The action from the parent report */ + parentReportAction?: OnyxEntry; + + /** The transaction from the parent report action */ + transaction: OnyxEntry; + + /** Comment added to report */ + comment: string; + + /** The receipt transaction from the parent report action */ + receiptTransactions: OnyxCollection; + + /** The reportID of the report */ + reportID: string; + + /** Array of report actions for this report */ + reportActions: OnyxEntry; + + /** List of transaction violation */ + transactionViolations: OnyxCollection; + + /** Whether the user can use violations */ + canUseViolations: boolean | undefined; +}; + +type OptionRowLHNProps = { + /** The ID of the report that the option is for */ + reportID: string; + + /** Whether this option is currently in focus so we can modify its style */ + isFocused?: boolean; + + /** A function that is called when an option is selected. Selected option is passed as a param */ + onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; + + /** Toggle between compact and default view */ + viewMode?: OptionMode; + + /** Additional style props */ + style?: StyleProp; + + /** The item that should be rendered */ + optionItem?: OptionData; +}; + +type RenderItemProps = {item: string}; + +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 00cf248ad838..24a647f900a7 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -4,35 +4,17 @@ import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; -import type {AvatarType} from '@src/types/onyx/OnyxCommon'; +import type {Icon} from '@src/types/onyx/OnyxCommon'; import Avatar from './Avatar'; import UserDetailsTooltip from './UserDetailsTooltip'; -type SubAvatar = { - /** Avatar source to display */ - source?: AvatarSource; - - /** Denotes whether it is an avatar or a workspace avatar */ - type?: AvatarType; - - /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name?: string; - - /** Avatar id */ - id?: number | string; - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: AvatarSource; -}; - type SubscriptAvatarProps = { /** Avatar URL or icon */ - mainAvatar?: SubAvatar; + mainAvatar?: Icon; /** Subscript avatar URL or icon */ - secondaryAvatar?: SubAvatar; + secondaryAvatar?: Icon; /** Set the size of avatars */ size?: ValueOf; @@ -47,7 +29,7 @@ type SubscriptAvatarProps = { showTooltip?: boolean; }; -function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AVATAR_SIZE.DEFAULT, backgroundColor, noMargin = false, showTooltip = true}: SubscriptAvatarProps) { +function SubscriptAvatar({mainAvatar, secondaryAvatar, size = CONST.AVATAR_SIZE.DEFAULT, backgroundColor, noMargin = false, showTooltip = true}: SubscriptAvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -59,23 +41,23 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index 26b3665ca4ce..5d925ae1c684 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -1,11 +1,12 @@ +import type {OnyxEntry} from 'react-native-onyx'; import type {Report} from '@src/types/onyx'; import * as ReportUtils from './ReportUtils'; /** * Returns the report name if the report is a group chat */ -function getGroupChatName(report: Report): string | undefined { - const participants = report.participantAccountIDs ?? []; +function getGroupChatName(report: OnyxEntry): string | undefined { + const participants = report?.participantAccountIDs ?? []; const isMultipleParticipantReport = participants.length > 1; return participants diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 91fa7b35e824..5b0464c58898 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -368,13 +368,12 @@ type CustomIcon = { type OptionData = { text: string; alternateText?: string | null; - allReportErrors?: Errors | null; + allReportErrors?: Errors; brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; tooltipText?: string | null; alternateTextMaxLines?: number; boldStyle?: boolean; customIcon?: CustomIcon; - descriptiveText?: string; subtitle?: string | null; login?: string | null; accountID?: number | null; @@ -395,8 +394,9 @@ type OptionData = { isAllowedToComment?: boolean | null; isThread?: boolean | null; isTaskReport?: boolean | null; - parentReportAction?: ReportAction; + parentReportAction?: OnyxEntry; displayNamesWithTooltips?: DisplayNameWithTooltips | null; + descriptiveText?: string; isDisabled?: boolean | null; name?: string | null; } & Report; @@ -3548,8 +3548,8 @@ function shouldHideReport(report: OnyxEntry, currentReportId: string): b /** * Checks to see if a report's parentAction is a money request that contains a violation */ -function doesTransactionThreadHaveViolations(report: Report, transactionViolations: OnyxCollection, parentReportAction: ReportAction): boolean { - if (parentReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { +function doesTransactionThreadHaveViolations(report: OnyxEntry, transactionViolations: OnyxCollection, parentReportAction: OnyxEntry): boolean { + if (parentReportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { return false; } const {IOUTransactionID, IOUReportID} = parentReportAction.originalMessage ?? {}; @@ -3559,7 +3559,7 @@ function doesTransactionThreadHaveViolations(report: Report, transactionViolatio if (!isCurrentUserSubmitter(IOUReportID)) { return false; } - if (report.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) { + if (report?.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report?.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) { return false; } return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations); @@ -4549,7 +4549,7 @@ function shouldDisplayThreadReplies(reportAction: OnyxEntry, repor * - The action is a whisper action and it's neither a report preview nor IOU action * - The action is the thread's first chat */ -function shouldDisableThread(reportAction: OnyxEntry, reportID: string) { +function shouldDisableThread(reportAction: OnyxEntry, reportID: string): boolean { const isSplitBillAction = ReportActionsUtils.isSplitBillAction(reportAction); const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 445d9dc30dd8..4a2c4a2da22a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,17 +1,17 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Str from 'expensify-common/lib/str'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, TransactionViolation} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; -import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; import * as CollectionUtils from './CollectionUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -257,12 +257,12 @@ function getOptionData({ parentReportAction, hasViolations, }: { - report: Report; - reportActions: Record; - personalDetails: Record; - preferredLocale: ValueOf; - policy: Policy; - parentReportAction: ReportAction; + report: OnyxEntry; + reportActions: OnyxEntry; + personalDetails: OnyxEntry; + preferredLocale: DeepValueOf; + policy: OnyxEntry | undefined; + parentReportAction: OnyxEntry | undefined; hasViolations: boolean; }): ReportUtils.OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for @@ -275,7 +275,7 @@ function getOptionData({ const result: ReportUtils.OptionData = { text: '', alternateText: null, - allReportErrors: null, + allReportErrors: undefined, brickRoadIndicator: null, tooltipText: null, subtitle: null, @@ -317,7 +317,8 @@ function getOptionData({ result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : undefined; - result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) as OnyxCommon.Errors; + // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. + result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); result.brickRoadIndicator = hasErrors || hasViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 8b040dd8d72c..ea8b3f258c89 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1,5 +1,5 @@ import {StyleSheet} from 'react-native'; -import type {Animated, DimensionValue, ImageStyle, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {Animated, ColorValue, DimensionValue, ImageStyle, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import type {ValueOf} from 'type-fest'; @@ -388,7 +388,7 @@ function getWidthStyle(width: number): ViewStyle { /** * Returns a style with backgroundColor and borderColor set to the same color */ -function getBackgroundAndBorderStyle(backgroundColor: string | undefined): ViewStyle { +function getBackgroundAndBorderStyle(backgroundColor: ColorValue | undefined): ViewStyle { return { backgroundColor, borderColor: backgroundColor, @@ -398,7 +398,7 @@ function getBackgroundAndBorderStyle(backgroundColor: string | undefined): ViewS /** * Returns a style with the specified backgroundColor */ -function getBackgroundColorStyle(backgroundColor: string): ViewStyle { +function getBackgroundColorStyle(backgroundColor: ColorValue): ViewStyle { return { backgroundColor, };