diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d9e57f5e3bb8..32575ee97ae5 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -106,7 +106,7 @@ function isExpensifyGuideTeam(email: string): boolean { */ const isPolicyAdmin = (policy: OnyxEntry | EmptyObject): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; -const isPolicyMember = (policyID: string, policies: Record): boolean => Object.values(policies).some((policy) => policy?.id === policyID); +const isPolicyMember = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID); /** * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 50fcbac34c96..d86e85b82e6b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4636,7 +4636,7 @@ function shouldAutoFocusOnKeyPress(event: KeyboardEvent): boolean { /** * Navigates to the appropriate screen based on the presence of a private note for the current user. */ -function navigateToPrivateNotes(report: Report, session: Session) { +function navigateToPrivateNotes(report: OnyxEntry, session: OnyxEntry) { if (isEmpty(report) || isEmpty(session) || !session.accountID) { return; } diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.tsx similarity index 66% rename from src/pages/ReportDetailsPage.js rename to src/pages/ReportDetailsPage.tsx index 99e1cea8280a..654e8330a582 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.tsx @@ -1,8 +1,9 @@ -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -11,89 +12,86 @@ import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; -import participantPropTypes from '@components/participantPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; +import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; -import reportPropTypes from './reportPropTypes'; -const propTypes = { - ...withLocalizePropTypes, - - /** The report currently being looked at */ - report: reportPropTypes.isRequired, - - /** The policies which the user has access to and which the report could be tied to */ - policies: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), - - /** Route params */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** Report ID passed via route r/:reportID/details */ - reportID: PropTypes.string, - }), - }).isRequired, +type ReportDetailsPageMenuItem = { + key: DeepValueOf; + translationKey: TranslationPaths; + icon: IconAsset; + isAnonymousAction: boolean; + action: () => void; + brickRoadIndicator?: ValueOf; + subtitle?: number; +}; +type ReportDetailsPageOnyxProps = { /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), -}; + personalDetails: OnyxCollection; -const defaultProps = { - policies: {}, - personalDetails: {}, + /** Session info for the currently logged in user. */ + session: OnyxEntry; }; +type ReportDetailsPageProps = ReportDetailsPageOnyxProps & WithReportOrNotFoundProps & StackScreenProps; -function ReportDetailsPage(props) { +function ReportDetailsPage({policies, report, session, personalDetails}: ReportDetailsPageProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const policy = useMemo(() => props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`], [props.policies, props.report.policyID]); - const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]); - const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(props.report.policyID, props.policies), [props.report.policyID, props.policies]); - const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(props.report), [props.report]); - const isChatRoom = useMemo(() => ReportUtils.isChatRoom(props.report), [props.report]); - const isThread = useMemo(() => ReportUtils.isChatThread(props.report), [props.report]); - const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(props.report), [props.report]); - const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(props.report), [props.report]); - const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(props.report), [props.report]); - const canEditReportDescription = useMemo(() => ReportUtils.canEditReportDescription(props.report, policy), [props.report, policy]); - const shouldShowReportDescription = isChatRoom && (canEditReportDescription || !_.isEmpty(props.report.description)); + const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]); + const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy ?? null), [policy]); + const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(report?.policyID ?? '', policies), [report?.policyID, policies]); + const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(report), [report]); + const isChatRoom = useMemo(() => ReportUtils.isChatRoom(report), [report]); + const isThread = useMemo(() => ReportUtils.isChatThread(report), [report]); + const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(report), [report]); + const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(report), [report]); + const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(report), [report]); + const canEditReportDescription = useMemo(() => ReportUtils.canEditReportDescription(report, policy), [report, policy]); + const shouldShowReportDescription = isChatRoom && (canEditReportDescription || report.description !== ''); // eslint-disable-next-line react-hooks/exhaustive-deps -- policy is a dependency because `getChatRoomSubtitle` calls `getPolicyName` which in turn retrieves the value from the `policy` value stored in Onyx - const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(props.report), [props.report, policy]); - const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); - const participants = useMemo(() => ReportUtils.getVisibleMemberIDs(props.report), [props.report]); + const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(report), [report, policy]); + const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); + const participants = useMemo(() => ReportUtils.getVisibleMemberIDs(report), [report]); - const isGroupDMChat = useMemo(() => ReportUtils.isDM(props.report) && participants.length > 1, [props.report, participants.length]); + const isGroupDMChat = useMemo(() => ReportUtils.isDM(report) && participants.length > 1, [report, participants.length]); - const isPrivateNotesFetchTriggered = !_.isUndefined(props.report.isLoadingPrivateNotes); + const isPrivateNotesFetchTriggered = report?.isLoadingPrivateNotes !== undefined; useEffect(() => { // Do not fetch private notes if isLoadingPrivateNotes is already defined, or if network is offline. - if (isPrivateNotesFetchTriggered || props.network.isOffline) { + if (isPrivateNotesFetchTriggered || isOffline) { return; } - Report.getReportPrivateNote(props.report.reportID); - }, [props.report.reportID, props.network.isOffline, isPrivateNotesFetchTriggered]); + Report.getReportPrivateNote(report?.reportID ?? ''); + }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered]); - const menuItems = useMemo(() => { - const items = []; + const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => { + const items: ReportDetailsPageMenuItem[] = []; if (!isGroupDMChat) { items.push({ @@ -101,7 +99,7 @@ function ReportDetailsPage(props) { translationKey: 'common.shareCode', icon: Expensicons.QrCode, isAnonymousAction: true, - action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(props.report.reportID)), + action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')), }); } @@ -120,21 +118,21 @@ function ReportDetailsPage(props) { subtitle: participants.length, isAnonymousAction: false, action: () => { - if (isUserCreatedPolicyRoom && !props.report.parentReportID) { - Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(props.report.reportID)); + if (isUserCreatedPolicyRoom && !report?.parentReportID) { + Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '')); } else { - Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '')); } }, }); - } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !props.report.parentReportID) { + } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !report?.parentReportID) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, translationKey: 'common.invite', icon: Expensicons.Users, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(report?.reportID ?? '')); }, }); } @@ -145,31 +143,31 @@ function ReportDetailsPage(props) { icon: Expensicons.Gear, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '')); }, }); // Prevent displaying private notes option for threads and task reports - if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(props.report)) { + if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES, translationKey: 'privateNotes.title', icon: Expensicons.Pencil, isAnonymousAction: false, - action: () => ReportUtils.navigateToPrivateNotes(props.report, props.session), - brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + action: () => ReportUtils.navigateToPrivateNotes(report, session), + brickRoadIndicator: Report.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } return items; - }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, props.report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, props.session]); + }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, session]); const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; - return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails), hasMultipleParticipants); - }, [participants, props.personalDetails]); + return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails), hasMultipleParticipants); + }, [participants, personalDetails]); - const icons = useMemo(() => ReportUtils.getIcons(props.report, props.personalDetails, null, '', -1, policy), [props.report, props.personalDetails, policy]); + const icons = useMemo(() => ReportUtils.getIcons(report, personalDetails, null, '', -1, policy), [report, personalDetails, policy]); const chatRoomSubtitleText = chatRoomSubtitle ? ( - + { Navigation.goBack(); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '')); }} /> @@ -203,14 +201,14 @@ function ReportDetailsPage(props) { ) : ( )} { - Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(props.report.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(report?.policyID ?? '')); }} > {chatRoomSubtitleText} @@ -233,41 +232,41 @@ function ReportDetailsPage(props) { ) : ( chatRoomSubtitleText )} - {!_.isEmpty(parentNavigationSubtitleData) && isMoneyRequestReport && ( + {!isEmptyObject(parentNavigationSubtitleData) && isMoneyRequestReport && ( )} {shouldShowReportDescription && ( - + Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(props.report.reportID))} + description={translate('reportDescriptionPage.roomDescription')} + onPress={() => Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID))} /> )} - {_.map(menuItems, (item) => { + {menuItems.map((item) => { const brickRoadIndicator = - ReportUtils.hasReportNameError(props.report) && item.key === CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + ReportUtils.hasReportNameError(report) && item.key === CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined; return ( ); })} @@ -278,22 +277,14 @@ function ReportDetailsPage(props) { } ReportDetailsPage.displayName = 'ReportDetailsPage'; -ReportDetailsPage.propTypes = propTypes; -ReportDetailsPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withReportOrNotFound(), - withNetwork(), - withOnyx({ +export default withReportOrNotFound()( + withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, session: { key: ONYXKEYS.SESSION, }, - }), -)(ReportDetailsPage); + })(ReportDetailsPage), +);