From 904fc05fe7764d17ebbf985df7c148b80cad509c Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Thu, 14 Mar 2024 13:02:25 -0600 Subject: [PATCH] Revert "[TS migration] Migrate 'Sidebar' page to TypeScript" --- src/libs/ReportUtils.ts | 9 +- src/libs/SidebarUtils.ts | 37 ++- src/libs/actions/Policy.ts | 4 +- src/libs/actions/Task.ts | 2 +- ...Status.tsx => AvatarWithOptionalStatus.js} | 20 +- .../index.tsx | 1 + ...or.tsx => PressableAvatarWithIndicator.js} | 55 ++-- .../{SidebarLinks.tsx => SidebarLinks.js} | 52 ++-- ...debarLinksData.tsx => SidebarLinksData.js} | 235 +++++++++++------- .../home/sidebar/SidebarNavigationContext.js | 0 ...SidebarScreen.tsx => BaseSidebarScreen.js} | 5 +- ....tsx => FloatingActionButtonAndPopover.js} | 142 ++++++----- .../SidebarScreen/{index.tsx => index.js} | 9 +- .../sidebar/SidebarScreen/sidebarPropTypes.js | 7 + .../{SignInButton.tsx => SignInButton.js} | 1 + tests/utils/LHNTestUtils.tsx | 3 +- 16 files changed, 359 insertions(+), 223 deletions(-) rename src/pages/home/sidebar/{AvatarWithOptionalStatus.tsx => AvatarWithOptionalStatus.js} (74%) rename src/pages/home/sidebar/{PressableAvatarWithIndicator.tsx => PressableAvatarWithIndicator.js} (53%) rename src/pages/home/sidebar/{SidebarLinks.tsx => SidebarLinks.js} (77%) rename src/pages/home/sidebar/{SidebarLinksData.tsx => SidebarLinksData.js} (60%) create mode 100644 src/pages/home/sidebar/SidebarNavigationContext.js rename src/pages/home/sidebar/SidebarScreen/{BaseSidebarScreen.tsx => BaseSidebarScreen.js} (90%) rename src/pages/home/sidebar/SidebarScreen/{FloatingActionButtonAndPopover.tsx => FloatingActionButtonAndPopover.js} (68%) rename src/pages/home/sidebar/SidebarScreen/{index.tsx => index.js} (51%) create mode 100644 src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js rename src/pages/home/sidebar/{SignInButton.tsx => SignInButton.js} (95%) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bf2a077736bf..8dc1c9967f13 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -718,8 +718,8 @@ function isOpenExpenseReport(report: OnyxEntry | EmptyObject): boolean { /** * Checks if the supplied report has a common policy member with the array passed in params. */ -function hasParticipantInArray(report: OnyxEntry, policyMemberAccountIDs: number[]) { - if (!report?.participantAccountIDs) { +function hasParticipantInArray(report: Report, policyMemberAccountIDs: number[]) { + if (!report.participantAccountIDs) { return false; } @@ -939,10 +939,9 @@ function isConciergeChatReport(report: OnyxEntry): boolean { * Checks if the supplied report belongs to workspace based on the provided params. If the report's policyID is _FAKE_ or has no value, it means this report is a DM. * In this case report and workspace members must be compared to determine whether the report belongs to the workspace. */ -function doesReportBelongToWorkspace(report: OnyxEntry, policyMemberAccountIDs: number[], policyID?: string) { +function doesReportBelongToWorkspace(report: Report, policyMemberAccountIDs: number[], policyID?: string) { return ( - isConciergeChatReport(report) || - (report?.policyID === CONST.POLICY.ID_FAKE || !report?.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : report?.policyID === policyID) + isConciergeChatReport(report) || (report.policyID === CONST.POLICY.ID_FAKE || !report.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : report.policyID === policyID) ); } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 81560e0d0691..7bf163416054 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -62,10 +62,10 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 { */ function getOrderedReportIDs( currentReportId: string | null, - allReports: OnyxCollection, - betas: OnyxEntry, - policies: OnyxCollection, - priorityMode: OnyxEntry>, + allReports: Record, + betas: Beta[], + policies: Record, + priorityMode: ValueOf, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', @@ -73,14 +73,15 @@ function getOrderedReportIDs( ): string[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReports ?? {}); + const allReportsDictValues = Object.values(allReports); + // Filter out all the reports that shouldn't be displayed let reportsToDisplay = allReportsDictValues.filter((report) => { const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`; const parentReportActions = allReportActions?.[parentReportActionsKey]; const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); const doesReportHaveViolations = - !!betas?.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', @@ -110,14 +111,14 @@ function getOrderedReportIDs( // 4. Archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode - const pinnedAndGBRReports: Array> = []; - const draftReports: Array> = []; - const nonArchivedReports: Array> = []; - const archivedReports: Array> = []; + const pinnedAndGBRReports: Report[] = []; + const draftReports: Report[] = []; + const nonArchivedReports: Report[] = []; + const archivedReports: Report[] = []; if (currentPolicyID || policyMemberAccountIDs.length > 0) { reportsToDisplay = reportsToDisplay.filter( - (report) => report?.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID), + (report) => report.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID), ); } // There are a few properties that need to be calculated for the report which are used when sorting reports. @@ -125,16 +126,14 @@ function getOrderedReportIDs( // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add // the reportDisplayName property to the report object directly. - if (report) { - // eslint-disable-next-line no-param-reassign - report.displayName = ReportUtils.getReportName(report); - } + // eslint-disable-next-line no-param-reassign + report.displayName = ReportUtils.getReportName(report); - const isPinned = report?.isPinned ?? false; - const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); + const isPinned = report.isPinned ?? false; + const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? ''); if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) { pinnedAndGBRReports.push(report); - } else if (report?.hasDraft) { + } else if (report.hasDraft) { draftReports.push(report); } else if (ReportUtils.isArchivedRoom(report)) { archivedReports.push(report); @@ -165,7 +164,7 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report?.reportID ?? ''); + const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); return LHNReports; } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index ad06173c48c1..0d7a4f97a9ad 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -249,8 +249,8 @@ function updateLastAccessedWorkspace(policyID: OnyxEntry) { /** * Check if the user has any active free policies (aka workspaces) */ -function hasActiveFreePolicy(policies: OnyxEntry): boolean { - const adminFreePolicies = Object.values(policies ?? {}).filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); +function hasActiveFreePolicy(policies: Array> | PoliciesRecord): boolean { + const adminFreePolicies = Object.values(policies).filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); if (adminFreePolicies.length === 0) { return false; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index fdbf10588c5d..48ab7cce9186 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -678,7 +678,7 @@ function setParentReportID(parentReportID: string) { /** * Clears out the task info from the store and navigates to the NewTaskDetails page */ -function clearOutTaskInfoAndNavigate(reportID?: string) { +function clearOutTaskInfoAndNavigate(reportID: string) { clearOutTaskInfo(); if (reportID && reportID !== '0') { setParentReportID(reportID); diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx b/src/pages/home/sidebar/AvatarWithOptionalStatus.js similarity index 74% rename from src/pages/home/sidebar/AvatarWithOptionalStatus.tsx rename to src/pages/home/sidebar/AvatarWithOptionalStatus.js index 548b0de2fb64..942d5c1da1f2 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.js @@ -1,3 +1,5 @@ +/* eslint-disable rulesdir/onyx-props-must-have-default */ +import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -7,18 +9,24 @@ import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import PressableAvatarWithIndicator from './PressableAvatarWithIndicator'; -type AvatarWithOptionalStatusProps = { +const propTypes = { /** Emoji status */ - emojiStatus?: string; + emojiStatus: PropTypes.string, /** Whether the avatar is selected */ - isSelected?: boolean; + isSelected: PropTypes.bool, /** Callback called when the avatar or status icon is pressed */ - onPress?: () => void; + onPress: PropTypes.func, }; -function AvatarWithOptionalStatus({emojiStatus = '', isSelected = false, onPress}: AvatarWithOptionalStatusProps) { +const defaultProps = { + emojiStatus: '', + isSelected: false, + onPress: () => {}, +}; + +function AvatarWithOptionalStatus({emojiStatus, isSelected, onPress}) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -45,5 +53,7 @@ function AvatarWithOptionalStatus({emojiStatus = '', isSelected = false, onPress ); } +AvatarWithOptionalStatus.propTypes = propTypes; +AvatarWithOptionalStatus.defaultProps = defaultProps; AvatarWithOptionalStatus.displayName = 'AvatarWithOptionalStatus'; export default AvatarWithOptionalStatus; diff --git a/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx index 33b89be8fd17..788dd4ae5bc8 100644 --- a/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx +++ b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx @@ -32,6 +32,7 @@ function BottomTabBarFloatingActionButton() { return ( diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.tsx b/src/pages/home/sidebar/PressableAvatarWithIndicator.js similarity index 53% rename from src/pages/home/sidebar/PressableAvatarWithIndicator.tsx rename to src/pages/home/sidebar/PressableAvatarWithIndicator.js index 53bae3d8a770..a7345ff6c14a 100644 --- a/src/pages/home/sidebar/PressableAvatarWithIndicator.tsx +++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.js @@ -1,34 +1,49 @@ +/* eslint-disable rulesdir/onyx-props-must-have-default */ +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import AvatarWithIndicator from '@components/AvatarWithIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import compose from '@libs/compose'; import * as UserUtils from '@libs/UserUtils'; +import personalDetailsPropType from '@pages/personalDetailsPropType'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -type PressableAvatarWithIndicatorOnyxProps = { +const propTypes = { + /** The personal details of the person who is logged in */ + currentUserPersonalDetails: personalDetailsPropType, + /** Indicates whether the app is loading initial data */ - isLoading: OnyxEntry; -}; + isLoading: PropTypes.bool, -type PressableAvatarWithIndicatorProps = PressableAvatarWithIndicatorOnyxProps & { /** Whether the avatar is selected */ - isSelected: boolean; + isSelected: PropTypes.bool, /** Callback called when the avatar is pressed */ - onPress?: () => void; + onPress: PropTypes.func, }; -function PressableAvatarWithIndicator({isLoading = true, isSelected = false, onPress}: PressableAvatarWithIndicatorProps) { +const defaultProps = { + currentUserPersonalDetails: { + pendingFields: {avatar: ''}, + accountID: '', + avatar: '', + }, + isLoading: true, + isSelected: false, + onPress: () => {}, +}; + +function PressableAvatarWithIndicator({currentUserPersonalDetails, isLoading, isSelected, onPress}) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); return ( - + @@ -50,10 +65,14 @@ function PressableAvatarWithIndicator({isLoading = true, isSelected = false, onP ); } +PressableAvatarWithIndicator.propTypes = propTypes; +PressableAvatarWithIndicator.defaultProps = defaultProps; PressableAvatarWithIndicator.displayName = 'PressableAvatarWithIndicator'; - -export default withOnyx({ - isLoading: { - key: ONYXKEYS.IS_LOADING_APP, - }, -})(PressableAvatarWithIndicator); +export default compose( + withCurrentUserPersonalDetails, + withOnyx({ + isLoading: { + key: ONYXKEYS.IS_LOADING_APP, + }, + }), +)(PressableAvatarWithIndicator); diff --git a/src/pages/home/sidebar/SidebarLinks.tsx b/src/pages/home/sidebar/SidebarLinks.js similarity index 77% rename from src/pages/home/sidebar/SidebarLinks.tsx rename to src/pages/home/sidebar/SidebarLinks.js index ef81aba715aa..4d1585cd424a 100644 --- a/src/pages/home/sidebar/SidebarLinks.tsx +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,8 +1,8 @@ +/* eslint-disable rulesdir/onyx-props-must-have-default */ +import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} 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'; +import _ from 'underscore'; import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import useLocalize from '@hooks/useLocalize'; @@ -13,40 +13,37 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import onyxSubscribe from '@libs/onyxSubscribe'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Modal, Report} from '@src/types/onyx'; -type SidebarLinksProps = { +const basePropTypes = { /** Toggles the navigation menu open and closed */ - onLinkClick: () => void; + onLinkClick: PropTypes.func.isRequired, /** Safe area insets required for mobile devices margins */ - insets: EdgeInsets; + insets: safeAreaInsetPropTypes.isRequired, +}; - /** List of options to display */ - optionListItems: string[]; +const propTypes = { + ...basePropTypes, - /** Wheather the reports are loading. When false it means they are ready to be used. */ - isLoading: OnyxEntry; + optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired, - /** The chat priority mode */ - priorityMode?: OnyxEntry>; + isLoading: PropTypes.bool.isRequired, - /** Method to change currently active report */ - isActiveReport: (reportID: string) => boolean; + // eslint-disable-next-line react/require-default-props + priorityMode: PropTypes.oneOf(_.values(CONST.PRIORITY_MODE)), - /** ID of currently active workspace */ - // eslint-disable-next-line react/no-unused-prop-types -- its used in withOnyx - activeWorkspaceID: string | undefined; + isActiveReport: PropTypes.func.isRequired, }; -function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport}: SidebarLinksProps) { +function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const modal = useRef({}); + const modal = useRef({}); const {updateLocale} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -64,7 +61,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority const unsubscribeOnyxModal = onyxSubscribe({ key: ONYXKEYS.MODAL, callback: (modalArg) => { - if (modalArg === null || typeof modalArg !== 'object') { + if (_.isNull(modalArg) || typeof modalArg !== 'object') { return; } modal.current = modalArg; @@ -102,21 +99,24 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority /** * Show Report page with selected report id + * + * @param {Object} option + * @param {String} option.reportID */ const showReportPage = useCallback( - (option: Report) => { + (option) => { // Prevent opening Report page when clicking LHN row quickly after clicking FAB icon // or when clicking the active LHN row on large screens // or when continuously clicking different LHNs, only apply to small screen // since getTopmostReportId always returns on other devices const reportActionID = Navigation.getTopmostReportActionId(); - if ((option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) { + if (isCreateMenuOpen || (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); onLinkClick(); }, - [isSmallScreenWidth, isActiveReport, onLinkClick], + [isCreateMenuOpen, isSmallScreenWidth, isActiveReport, onLinkClick], ); const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; @@ -136,7 +136,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority optionMode={viewMode} onFirstItemRendered={App.setSidebarLoaded} /> - {isLoading && optionListItems?.length === 0 && ( + {isLoading && optionListItems.length === 0 && ( @@ -146,6 +146,8 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority ); } +SidebarLinks.propTypes = propTypes; SidebarLinks.displayName = 'SidebarLinks'; export default SidebarLinks; +export {basePropTypes}; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.js similarity index 60% rename from src/pages/home/sidebar/SidebarLinksData.tsx rename to src/pages/home/sidebar/SidebarLinksData.js index 9ac490a29a7d..c4cc0713c596 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -1,114 +1,162 @@ -import {useIsFocused} from '@react-navigation/native'; import {deepEqual} from 'fast-equals'; +import lodashGet from 'lodash/get'; +import lodashMap from 'lodash/map'; +import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {EdgeInsets} from 'react-native-safe-area-context'; -import type {ValueOf} from 'type-fest'; -import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; +import _ from 'underscore'; +import networkPropTypes from '@components/networkPropTypes'; +import {withNetwork} from '@components/OnyxProvider'; import withCurrentReportID from '@components/withCurrentReportID'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import withNavigationFocus from '@components/withNavigationFocus'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import compose from '@libs/compose'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; +import reportPropTypes from '@pages/reportPropTypes'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {Message} from '@src/types/onyx/ReportAction'; -import SidebarLinks from './SidebarLinks'; +import SidebarLinks, {basePropTypes} from './SidebarLinks'; -type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; -type PolicySelector = Pick; -type ReportActionsSelector = Array>; +const propTypes = { + ...basePropTypes, -type SidebarLinksDataOnyxProps = { + /* Onyx Props */ /** List of reports */ - chatReports: OnyxCollection; + chatReports: PropTypes.objectOf(reportPropTypes), - /** Wheather the reports are loading. When false it means they are ready to be used. */ - isLoadingApp: OnyxEntry; + /** All report actions for all reports */ + + /** Object of report actions for this report */ + allReportActions: PropTypes.objectOf( + PropTypes.arrayOf( + PropTypes.shape({ + error: PropTypes.string, + message: PropTypes.arrayOf( + PropTypes.shape({ + moderationDecision: PropTypes.shape({ + decision: PropTypes.string, + }), + }), + ), + }), + ), + ), + + /** Whether the reports are loading. When false it means they are ready to be used. */ + isLoadingApp: PropTypes.bool, /** The chat priority mode */ - priorityMode: OnyxEntry>; + priorityMode: PropTypes.string, /** Beta features list */ - betas: OnyxEntry; + betas: PropTypes.arrayOf(PropTypes.string), - /** All report actions for all reports */ - allReportActions: OnyxCollection; + network: networkPropTypes.isRequired, /** The policies which the user has access to */ - policies: OnyxCollection; + // eslint-disable-next-line react/forbid-prop-types + policies: PropTypes.object, + + // eslint-disable-next-line react/forbid-prop-types + policyMembers: PropTypes.object, /** All of the transaction violations */ - transactionViolations: OnyxCollection; + transactionViolations: PropTypes.shape({ + violations: PropTypes.arrayOf( + PropTypes.shape({ + /** The transaction ID */ + transactionID: PropTypes.number, - /** All policy members */ - policyMembers: OnyxCollection; -}; + /** The transaction violation type */ + type: PropTypes.string, -type SidebarLinksDataProps = CurrentReportIDContextValue & - SidebarLinksDataOnyxProps & { - /** Toggles the navigation menu open and closed */ - onLinkClick: () => void; + /** The transaction violation message */ + message: PropTypes.string, - /** Safe area insets required for mobile devices margins */ - insets: EdgeInsets; - }; + /** The transaction violation data */ + data: PropTypes.shape({ + /** The transaction violation data field */ + field: PropTypes.string, + + /** The transaction violation data value */ + value: PropTypes.string, + }), + }), + ), + }), + + ...withCurrentUserPersonalDetailsPropTypes, +}; + +const defaultProps = { + chatReports: {}, + isLoadingApp: true, + priorityMode: CONST.PRIORITY_MODE.DEFAULT, + betas: [], + policies: {}, + policyMembers: {}, + transactionViolations: {}, + allReportActions: {}, + ...withCurrentUserPersonalDetailsDefaultProps, +}; function SidebarLinksData({ + isFocused, allReportActions, betas, chatReports, + currentReportID, insets, - isLoadingApp = true, + isLoadingApp, onLinkClick, policies, - priorityMode = CONST.PRIORITY_MODE.DEFAULT, + priorityMode, + network, policyMembers, transactionViolations, - currentReportID, -}: SidebarLinksDataProps) { - const {accountID} = useCurrentUserPersonalDetails(); - const network = useNetwork(); - const isFocused = useIsFocused(); + currentUserPersonalDetails, +}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); const prevPriorityMode = usePrevious(priorityMode); - const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, accountID); + + const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, currentUserPersonalDetails.accountID); + // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => Policy.openWorkspace(activeWorkspaceID ?? '', policyMemberAccountIDs), [activeWorkspaceID]); - const reportIDsRef = useRef(null); + useEffect(() => Policy.openWorkspace(activeWorkspaceID, policyMemberAccountIDs), [activeWorkspaceID]); + + const reportIDsRef = useRef(null); const isLoading = isLoadingApp; - const optionListItems: string[] = useMemo(() => { + const optionListItems = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs( null, - chatReports as OnyxCollection, + chatReports, betas, - policies as OnyxCollection, + policies, priorityMode, - allReportActions as OnyxCollection, + allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, ); - if (reportIDsRef.current && deepEqual(reportIDsRef.current, reportIDs)) { + if (deepEqual(reportIDsRef.current, reportIDs)) { return reportIDsRef.current; } // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !reportIDsRef.current || !!network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { + if (!isLoading || !reportIDsRef.current || network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; @@ -120,14 +168,14 @@ function SidebarLinksData({ // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportID && !optionListItems?.includes(currentReportID)) { + if (currentReportID && !_.contains(optionListItems, currentReportID)) { return SidebarUtils.getOrderedReportIDs( currentReportID, chatReports, betas, - policies as OnyxCollection, + policies, priorityMode, - allReportActions as OnyxCollection, + allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -138,7 +186,7 @@ function SidebarLinksData({ const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; - const isActiveReport = useCallback((reportID: string): boolean => currentReportIDRef.current === reportID, []); + const isActiveReport = useCallback((reportID) => currentReportIDRef.current === reportID, []); return ( ): ChatReportSelector => - (report && { +const chatReportSelector = (report) => + report && { reportID: report.reportID, participantAccountIDs: report.participantAccountIDs, hasDraft: report.hasDraft, @@ -176,7 +228,7 @@ const chatReportSelector = (report: OnyxEntry): ChatReportSele isHidden: report.isHidden, notificationPreference: report.notificationPreference, errorFields: { - addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, + addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, }, lastMessageText: report.lastMessageText, lastVisibleActionCreated: report.lastVisibleActionCreated, @@ -205,36 +257,49 @@ const chatReportSelector = (report: OnyxEntry): ChatReportSele parentReportID: report.parentReportID, isDeletedParentAction: report.isDeletedParentAction, isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }) as ChatReportSelector; - -const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => - (reportActions && - Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = reportAction.message?.[0].moderationDecision?.decision; - - return { - reportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ] as Message[], - originalMessage, - }; - })) as ReportActionsSelector; - -const policySelector = (policy: OnyxEntry): PolicySelector => - (policy && { + }; + +/** + * @param {Object} [reportActions] + * @returns {Object|undefined} + */ +const reportActionsSelector = (reportActions) => + reportActions && + lodashMap(reportActions, (reportAction) => { + const {reportActionID, parentReportActionID, actionName, errors = [], originalMessage} = reportAction; + const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); + + return { + reportActionID, + parentReportActionID, + actionName, + errors, + message: [ + { + moderationDecision: {decision}, + }, + ], + originalMessage, + }; + }); + +/** + * @param {Object} [policy] + * @returns {Object|undefined} + */ +const policySelector = (policy) => + policy && { type: policy.type, name: policy.name, avatar: policy.avatar, - }) as PolicySelector; + }; -export default withCurrentReportID( - withOnyx({ +export default compose( + withCurrentReportID, + withCurrentUserPersonalDetails, + withNavigationFocus, + withNetwork(), + withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, selector: chatReportSelector, @@ -268,7 +333,5 @@ export default withCurrentReportID( key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, initialValue: {}, }, - })(SidebarLinksData), -); - -export type {PolicySelector}; + }), +)(SidebarLinksData); diff --git a/src/pages/home/sidebar/SidebarNavigationContext.js b/src/pages/home/sidebar/SidebarNavigationContext.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js similarity index 90% rename from src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx rename to src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index 1cd59fe17716..2c2d28a0edbc 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -9,6 +9,7 @@ import Performance from '@libs/Performance'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; +import sidebarPropTypes from './sidebarPropTypes'; /** * Function called when a pinned chat is selected. @@ -18,7 +19,7 @@ const startTimer = () => { Performance.markStart(CONST.TIMING.SWITCH_REPORT); }; -function BaseSidebarScreen() { +function BaseSidebarScreen(props) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); useEffect(() => { @@ -41,6 +42,7 @@ function BaseSidebarScreen() { @@ -49,6 +51,7 @@ function BaseSidebarScreen() { ); } +BaseSidebarScreen.propTypes = sidebarPropTypes; BaseSidebarScreen.displayName = 'BaseSidebarScreen'; export default BaseSidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js similarity index 68% rename from src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx rename to src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 90a4fc6fffa0..573cbe370aa7 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -1,20 +1,20 @@ -import {useIsFocused} from '@react-navigation/native'; -import type {ForwardedRef, RefAttributes} from 'react'; +import PropTypes from 'prop-types'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FloatingActionButton from '@components/FloatingActionButton'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; +import withNavigation from '@components/withNavigation'; +import withNavigationFocus from '@components/withNavigationFocus'; +import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; +import compose from '@libs/compose'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; -import type {PolicySelector} from '@pages/home/sidebar/SidebarLinksData'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy'; @@ -22,54 +22,74 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -type FloatingActionButtonAndPopoverOnyxProps = { - /** The list of policies the user has access to. */ - allPolicies: OnyxCollection; +/** + * @param {Object} [policy] + * @returns {Object|undefined} + */ +const policySelector = (policy) => + policy && { + type: policy.type, + role: policy.role, + isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, + pendingAction: policy.pendingAction, + }; - /** Whether app is in loading state */ - isLoading: OnyxEntry; -}; +const propTypes = { + ...windowDimensionsPropTypes, -type FloatingActionButtonAndPopoverProps = FloatingActionButtonAndPopoverOnyxProps & { /* Callback function when the menu is shown */ - onShowCreateMenu?: () => void; + onShowCreateMenu: PropTypes.func, /* Callback function before the menu is hidden */ - onHideCreateMenu?: () => void; -}; + onHideCreateMenu: PropTypes.func, -type FloatingActionButtonAndPopoverRef = { - hideCreateMenu: () => void; + /** The list of policies the user has access to. */ + allPolicies: PropTypes.shape({ + /** The policy name */ + name: PropTypes.string, + }), + + /** Indicated whether the report data is loading */ + isLoading: PropTypes.bool, + + /** Forwarded ref to FloatingActionButtonAndPopover */ + innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), +}; +const defaultProps = { + onHideCreateMenu: () => {}, + onShowCreateMenu: () => {}, + allPolicies: {}, + isLoading: false, + innerRef: null, }; /** * Responsible for rendering the {@link PopoverMenu}, and the accompanying * FAB that can open or close the menu. + * @param {Object} props + * @returns {JSX.Element} */ -function FloatingActionButtonAndPopover( - {onHideCreateMenu, onShowCreateMenu, isLoading, allPolicies}: FloatingActionButtonAndPopoverProps, - ref: ForwardedRef, -) { +function FloatingActionButtonAndPopover(props) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); - const fabRef = useRef(null); - const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); - const isFocused = useIsFocused(); + const fabRef = useRef(null); - const prevIsFocused = usePrevious(isFocused); + const prevIsFocused = usePrevious(props.isFocused); /** * Check if LHN status changed from active to inactive. * Used to close already opened FAB menu when open any other pages (i.e. Press Command + K on web). + * + * @param {Object} prevProps + * @return {Boolean} */ const didScreenBecomeInactive = useCallback( () => // When any other page is opened over LHN - !isFocused && prevIsFocused, - [isFocused, prevIsFocused], + !props.isFocused && prevIsFocused, + [props.isFocused, prevIsFocused], ); /** @@ -77,14 +97,14 @@ function FloatingActionButtonAndPopover( */ const showCreateMenu = useCallback( () => { - if (!isFocused && isSmallScreenWidth) { + if (!props.isFocused && props.isSmallScreenWidth) { return; } setIsCreateMenuActive(true); - onShowCreateMenu?.(); + props.onShowCreateMenu(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [isFocused, isSmallScreenWidth], + [props.isFocused, props.isSmallScreenWidth], ); /** @@ -98,7 +118,7 @@ function FloatingActionButtonAndPopover( return; } setIsCreateMenuActive(false); - onHideCreateMenu?.(); + props.onHideCreateMenu(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [isCreateMenuActive], @@ -113,7 +133,7 @@ function FloatingActionButtonAndPopover( hideCreateMenu(); }, [didScreenBecomeInactive, hideCreateMenu]); - useImperativeHandle(ref, () => ({ + useImperativeHandle(props.innerRef, () => ({ hideCreateMenu() { hideCreateMenu(); }, @@ -131,10 +151,10 @@ function FloatingActionButtonAndPopover( interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)), }, - ...(!isLoading && !Policy.hasActiveFreePolicy(allPolicies as OnyxEntry>) + ...(!props.isLoading && !Policy.hasActiveFreePolicy(props.allPolicies) ? [ { displayInDefaultIconColor: true, - contentFit: 'contain' as const, + contentFit: 'contain', icon: Expensicons.NewWorkspace, iconWidth: 46, iconHeight: 40, @@ -200,25 +220,31 @@ function FloatingActionButtonAndPopover( ); } +FloatingActionButtonAndPopover.propTypes = propTypes; +FloatingActionButtonAndPopover.defaultProps = defaultProps; FloatingActionButtonAndPopover.displayName = 'FloatingActionButtonAndPopover'; -const policySelector = (policy: OnyxEntry) => - policy - ? { - type: policy.type, - role: policy.role, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, - pendingAction: policy.pendingAction, - } - : null; - -export default withOnyx, FloatingActionButtonAndPopoverOnyxProps>({ - allPolicies: { - key: ONYXKEYS.COLLECTION.POLICY, - // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but for collection keys the selector is executed for each collection item. This is a bug in withOnyx typings that we don't have a solution yet, when useOnyx hook is introduced it will be fixed. - selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection, - }, - isLoading: { - key: ONYXKEYS.IS_LOADING_APP, - }, -})(forwardRef(FloatingActionButtonAndPopover)); +const FloatingActionButtonAndPopoverWithRef = forwardRef((props, ref) => ( + +)); + +FloatingActionButtonAndPopoverWithRef.displayName = 'FloatingActionButtonAndPopoverWithRef'; + +export default compose( + withNavigation, + withNavigationFocus, + withWindowDimensions, + withOnyx({ + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + selector: policySelector, + }, + isLoading: { + key: ONYXKEYS.IS_LOADING_APP, + }, + }), +)(FloatingActionButtonAndPopoverWithRef); diff --git a/src/pages/home/sidebar/SidebarScreen/index.tsx b/src/pages/home/sidebar/SidebarScreen/index.js similarity index 51% rename from src/pages/home/sidebar/SidebarScreen/index.tsx rename to src/pages/home/sidebar/SidebarScreen/index.js index 625491674cd8..dfb5db7c15d3 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.tsx +++ b/src/pages/home/sidebar/SidebarScreen/index.js @@ -1,15 +1,20 @@ import React from 'react'; import FreezeWrapper from '@libs/Navigation/FreezeWrapper'; import BaseSidebarScreen from './BaseSidebarScreen'; +import sidebarPropTypes from './sidebarPropTypes'; -function SidebarScreen() { +function SidebarScreen(props) { return ( - + ); } +SidebarScreen.propTypes = sidebarPropTypes; SidebarScreen.displayName = 'SidebarScreen'; export default SidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js new file mode 100644 index 000000000000..61a9194bb1e5 --- /dev/null +++ b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js @@ -0,0 +1,7 @@ +import PropTypes from 'prop-types'; + +const sidebarPropTypes = { + /** Callback when onLayout of sidebar is called */ + onLayout: PropTypes.func, +}; +export default sidebarPropTypes; diff --git a/src/pages/home/sidebar/SignInButton.tsx b/src/pages/home/sidebar/SignInButton.js similarity index 95% rename from src/pages/home/sidebar/SignInButton.tsx rename to src/pages/home/sidebar/SignInButton.js index 1dc65bfd5050..f89deb6f65b2 100644 --- a/src/pages/home/sidebar/SignInButton.tsx +++ b/src/pages/home/sidebar/SignInButton.js @@ -1,3 +1,4 @@ +/* eslint-disable rulesdir/onyx-props-must-have-default */ import React from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 3aa428cc3eef..85c2d67f80bc 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -282,6 +282,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { return ( {}} insets={{ top: 0, @@ -289,7 +290,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { right: 0, bottom: 0, }} - // @ts-expect-error - normally this comes from withCurrentReportID hoc, but here we are just mocking this + isSmallScreenWidth={false} currentReportID={currentReportID} />