From 32f95b62675a9e868a6ab01ea653657d3a53a5e9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 11 Dec 2023 16:17:06 +0100 Subject: [PATCH 001/159] [TS migration] Migrate 'VideoChatButtonAndMenu' component to TypeScript --- src/components/Popover/types.ts | 15 +++---- src/components/PopoverProvider/index.tsx | 39 +++++++++------- src/components/PopoverProvider/types.ts | 13 +++--- ...Menu.js => BaseVideoChatButtonAndMenu.tsx} | 44 ++++++++----------- .../{index.android.js => index.android.tsx} | 7 ++- .../{index.js => index.tsx} | 7 ++- .../VideoChatButtonAndMenu/types.ts | 9 ++++ .../videoChatButtonAndMenuPropTypes.js | 16 ------- 8 files changed, 71 insertions(+), 79 deletions(-) rename src/components/VideoChatButtonAndMenu/{BaseVideoChatButtonAndMenu.js => BaseVideoChatButtonAndMenu.tsx} (75%) rename src/components/VideoChatButtonAndMenu/{index.android.js => index.android.tsx} (76%) rename src/components/VideoChatButtonAndMenu/{index.js => index.tsx} (68%) create mode 100644 src/components/VideoChatButtonAndMenu/types.ts delete mode 100644 src/components/VideoChatButtonAndMenu/videoChatButtonAndMenuPropTypes.js diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 7f7e2829770c..103ab0404081 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -1,8 +1,8 @@ +import {ReactNode, RefObject} from 'react'; +import {View} from 'react-native'; import BaseModalProps, {PopoverAnchorPosition} from '@components/Modal/types'; import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; -type AnchorAlignment = {horizontal: string; vertical: string}; - type PopoverDimensions = { width: number; height: number; @@ -12,14 +12,11 @@ type PopoverProps = BaseModalProps & { /** The anchor position of the popover */ anchorPosition?: PopoverAnchorPosition; - /** The anchor alignment of the popover */ - anchorAlignment: AnchorAlignment; - /** The anchor ref of the popover */ - anchorRef: React.RefObject; + anchorRef: RefObject; /** Whether disable the animations */ - disableAnimation: boolean; + disableAnimation?: boolean; /** Whether we don't want to show overlay */ withoutOverlay: boolean; @@ -28,13 +25,13 @@ type PopoverProps = BaseModalProps & { popoverDimensions?: PopoverDimensions; /** The ref of the popover */ - withoutOverlayRef?: React.RefObject; + withoutOverlayRef?: RefObject; /** Whether we want to show the popover on the right side of the screen */ fromSidebarMediumScreen?: boolean; /** The popover children */ - children: React.ReactNode; + children: ReactNode; }; type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps; diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx index 06345ebdbc1c..1be7c7a95bc1 100644 --- a/src/components/PopoverProvider/index.tsx +++ b/src/components/PopoverProvider/index.tsx @@ -1,18 +1,26 @@ -import React from 'react'; +import React, {createContext, RefObject, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; import {AnchorRef, PopoverContextProps, PopoverContextValue} from './types'; -const PopoverContext = React.createContext({ +const PopoverContext = createContext({ onOpen: () => {}, popover: {}, close: () => {}, isOpen: false, }); +function elementContains(ref: RefObject | undefined, target: EventTarget | null) { + if (ref?.current && 'contains' in ref?.current && ref?.current?.contains(target as Node)) { + return true; + } + return false; +} + function PopoverContextProvider(props: PopoverContextProps) { - const [isOpen, setIsOpen] = React.useState(false); - const activePopoverRef = React.useRef(null); + const [isOpen, setIsOpen] = useState(false); + const activePopoverRef = useRef(null); - const closePopover = React.useCallback((anchorRef?: React.RefObject) => { + const closePopover = useCallback((anchorRef?: RefObject) => { if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) { return; } @@ -25,10 +33,9 @@ function PopoverContextProvider(props: PopoverContextProps) { setIsOpen(false); }, []); - React.useEffect(() => { + useEffect(() => { const listener = (e: Event) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (activePopoverRef.current?.ref?.current?.contains(e.target as Node) || activePopoverRef.current?.anchorRef?.current?.contains(e.target as Node)) { + if (elementContains(activePopoverRef.current?.ref, e.target) || elementContains(activePopoverRef.current?.anchorRef, e.target)) { return; } const ref = activePopoverRef.current?.anchorRef; @@ -40,9 +47,9 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = (e: Event) => { - if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { + if (elementContains(activePopoverRef.current?.ref, e.target)) { return; } closePopover(); @@ -53,7 +60,7 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = (e: KeyboardEvent) => { if (e.key !== 'Escape') { return; @@ -66,7 +73,7 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = () => { if (document.hasFocus()) { return; @@ -79,9 +86,9 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - React.useEffect(() => { + useEffect(() => { const listener = (e: Event) => { - if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { + if (elementContains(activePopoverRef.current?.ref, e.target)) { return; } @@ -93,7 +100,7 @@ function PopoverContextProvider(props: PopoverContextProps) { }; }, [closePopover]); - const onOpen = React.useCallback( + const onOpen = useCallback( (popoverParams: AnchorRef) => { if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) { closePopover(activePopoverRef.current.anchorRef); @@ -107,7 +114,7 @@ function PopoverContextProvider(props: PopoverContextProps) { [closePopover], ); - const contextValue = React.useMemo( + const contextValue = useMemo( () => ({ onOpen, close: closePopover, diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts index ffd0087cd5ff..dc0208e10dd7 100644 --- a/src/components/PopoverProvider/types.ts +++ b/src/components/PopoverProvider/types.ts @@ -1,18 +1,21 @@ +import {ReactNode, RefObject} from 'react'; +import {View} from 'react-native'; + type PopoverContextProps = { - children: React.ReactNode; + children: ReactNode; }; type PopoverContextValue = { onOpen?: (popoverParams: AnchorRef) => void; popover?: AnchorRef | Record | null; - close: (anchorRef?: React.RefObject) => void; + close: (anchorRef?: RefObject) => void; isOpen: boolean; }; type AnchorRef = { - ref: React.RefObject; - close: (anchorRef?: React.RefObject) => void; - anchorRef: React.RefObject; + ref: RefObject; + close: (anchorRef?: RefObject) => void; + anchorRef: RefObject; onOpenCallback?: () => void; onCloseCallback?: () => void; }; diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx similarity index 75% rename from src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js rename to src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx index 4d5affafc407..14dbcb9a118b 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx @@ -1,7 +1,5 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Dimensions, View} from 'react-native'; -import _ from 'underscore'; import GoogleMeetIcon from '@assets/images/google-meet.svg'; import ZoomIcon from '@assets/images/zoom-icon.svg'; import Icon from '@components/Icon'; @@ -10,37 +8,34 @@ import MenuItem from '@components/MenuItem'; import Popover from '@components/Popover'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import * as Link from '@userActions/Link'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; -import {defaultProps, propTypes as videoChatButtonAndMenuPropTypes} from './videoChatButtonAndMenuPropTypes'; +import VideoChatButtonAndMenuProps from './types'; -const propTypes = { +type BaseVideoChatButtonAndMenuProps = VideoChatButtonAndMenuProps & { /** Link to open when user wants to create a new google meet meeting */ - googleMeetURL: PropTypes.string.isRequired, - - ...videoChatButtonAndMenuPropTypes, - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, + googleMeetURL: string; }; -function BaseVideoChatButtonAndMenu(props) { +function BaseVideoChatButtonAndMenu(props: BaseVideoChatButtonAndMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); const [isVideoChatMenuActive, setIsVideoChatMenuActive] = useState(false); const [videoChatIconPosition, setVideoChatIconPosition] = useState({x: 0, y: 0}); - const videoChatIconWrapperRef = useRef(null); - const videoChatButtonRef = useRef(null); + const videoChatIconWrapperRef = useRef(null); + const videoChatButtonRef = useRef(null); const menuItemData = [ { icon: ZoomIcon, - text: props.translate('videoChatButtonAndMenu.zoom'), + text: translate('videoChatButtonAndMenu.zoom'), onPress: () => { setIsVideoChatMenuActive(false); Link.openExternalLink(CONST.NEW_ZOOM_MEETING_URL); @@ -48,7 +43,7 @@ function BaseVideoChatButtonAndMenu(props) { }, { icon: GoogleMeetIcon, - text: props.translate('videoChatButtonAndMenu.googleMeet'), + text: translate('videoChatButtonAndMenu.googleMeet'), onPress: () => { setIsVideoChatMenuActive(false); Link.openExternalLink(props.googleMeetURL); @@ -87,12 +82,12 @@ function BaseVideoChatButtonAndMenu(props) { ref={videoChatIconWrapperRef} onLayout={measureVideoChatIconPosition} > - + { // Drop focus to avoid blue focus ring. - videoChatButtonRef.current.blur(); + videoChatButtonRef.current?.blur(); // If this is the Concierge chat, we'll open the modal for requesting a setup call instead if (props.isConcierge && props.guideCalendarLink) { @@ -102,7 +97,7 @@ function BaseVideoChatButtonAndMenu(props) { setIsVideoChatMenuActive((previousVal) => !previousVal); })} style={styles.touchableButtonImage} - accessibilityLabel={props.translate('videoChatButtonAndMenu.tooltip')} + accessibilityLabel={translate('videoChatButtonAndMenu.tooltip')} role={CONST.ACCESSIBILITY_ROLE.BUTTON} > - - {_.map(menuItemData, ({icon, text, onPress}) => ( + + {menuItemData.map(({icon, text, onPress}) => ( Date: Mon, 11 Dec 2023 18:19:39 +0100 Subject: [PATCH 002/159] [TS migration] Migrate 'ShowContextMenuContext.js' component to TypeScript --- ...MenuContext.js => ShowContextMenuContext.tsx} | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) rename src/components/{ShowContextMenuContext.js => ShowContextMenuContext.tsx} (67%) diff --git a/src/components/ShowContextMenuContext.js b/src/components/ShowContextMenuContext.tsx similarity index 67% rename from src/components/ShowContextMenuContext.js rename to src/components/ShowContextMenuContext.tsx index 6248478e5fea..b2c1835d59e7 100644 --- a/src/components/ShowContextMenuContext.js +++ b/src/components/ShowContextMenuContext.tsx @@ -3,6 +3,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import * as ContextMenuActions from '@pages/home/report/ContextMenu/ContextMenuActions'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import ReportAction from '@src/types/onyx/ReportAction'; const ShowContextMenuContext = React.createContext({ anchor: null, @@ -16,17 +17,18 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; /** * Show the report action context menu. * - * @param {Object} event - Press event object - * @param {Element} anchor - Context menu anchor - * @param {String} reportID - Active Report ID - * @param {Object} action - ReportAction for ContextMenu - * @param {Function} checkIfContextMenuActive Callback to update context menu active state - * @param {Boolean} [isArchivedRoom=false] - Is the report an archived room + * @param event - Press event object + * @param anchor - Context menu anchor + * @param reportID - Active Report ID + * @param action - ReportAction for ContextMenu + * @param checkIfContextMenuActive Callback to update context menu active state + * @param isArchivedRoom - Is the report an archived room */ -function showContextMenuForReport(event, anchor, reportID, action, checkIfContextMenuActive, isArchivedRoom = false) { +function showContextMenuForReport(event: Event, anchor: HTMLElement, reportID: string, action: ReportAction, checkIfContextMenuActive: () => void, isArchivedRoom = false) { if (!DeviceCapabilities.canUseTouchScreen()) { return; } + ReportActionContextMenu.showContextMenu( ContextMenuActions.CONTEXT_MENU_TYPES.REPORT_ACTION, event, From ae0648c0b4a0565aecb7e8958859d4e1231f87e5 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 13 Dec 2023 18:37:06 +0000 Subject: [PATCH 003/159] refactor(typescript): migrate conciergepage --- .../{ConciergePage.js => ConciergePage.tsx} | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) rename src/pages/{ConciergePage.js => ConciergePage.tsx} (67%) diff --git a/src/pages/ConciergePage.js b/src/pages/ConciergePage.tsx similarity index 67% rename from src/pages/ConciergePage.js rename to src/pages/ConciergePage.tsx index 841ce524b2cb..ffb82f689620 100644 --- a/src/pages/ConciergePage.js +++ b/src/pages/ConciergePage.tsx @@ -1,36 +1,28 @@ import {useFocusEffect} from '@react-navigation/native'; -import PropTypes from 'prop-types'; import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {Session} from '@src/types/onyx'; -const propTypes = { +type ConciergePageOnyxProps = { /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), + session: OnyxEntry; }; -const defaultProps = { - session: { - authToken: null, - }, -}; +type ConciergePageProps = ConciergePageOnyxProps; /* * This is a "utility page", that does this: * - If the user is authenticated, find their concierge chat and re-route to it * - Else re-route to the login page */ -function ConciergePage(props) { +function ConciergePage({session}: ConciergePageProps) { useFocusEffect(() => { - if (_.has(props.session, 'authToken')) { + if (session?.authToken) { // Pop the concierge loading page before opening the concierge report. Navigation.isNavigationReady().then(() => { Navigation.goBack(ROUTES.HOME); @@ -43,12 +35,9 @@ function ConciergePage(props) { return ; } - -ConciergePage.propTypes = propTypes; -ConciergePage.defaultProps = defaultProps; ConciergePage.displayName = 'ConciergePage'; -export default withOnyx({ +export default withOnyx({ session: { key: ONYXKEYS.SESSION, }, From d24c8f1b6c82de2cf89cc3657cc3aef9a290f7c7 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 14 Dec 2023 13:16:33 +0000 Subject: [PATCH 004/159] refactor(typescript): migrate loginwithshortlivedauthtoken --- src/libs/Navigation/types.ts | 5 +- ...s => LogInWithShortLivedAuthTokenPage.tsx} | 61 ++++++------------- 2 files changed, 23 insertions(+), 43 deletions(-) rename src/pages/{LogInWithShortLivedAuthTokenPage.js => LogInWithShortLivedAuthTokenPage.tsx} (64%) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 94a07ddc6b73..1a7f017cf4d8 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -3,6 +3,7 @@ import {CommonActions, NavigationContainerRefWithCurrent, NavigationHelpers, Nav import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; +import type {Route as Routes} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; type NavigationRef = NavigationContainerRefWithCurrent; @@ -366,8 +367,10 @@ type PublicScreensParamList = { [SCREENS.TRANSITION_BETWEEN_APPS]: { shouldForceLogin: string; email: string; + error: string; shortLivedAuthToken: string; - exitTo: string; + shortLivedToken: string; + exitTo: Routes; }; [SCREENS.VALIDATE_LOGIN]: { accountID: string; diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.js b/src/pages/LogInWithShortLivedAuthTokenPage.tsx similarity index 64% rename from src/pages/LogInWithShortLivedAuthTokenPage.js rename to src/pages/LogInWithShortLivedAuthTokenPage.tsx index 16d0c3909d62..c262b4d14658 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.js +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -1,8 +1,7 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -11,64 +10,44 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; +import {PublicScreensParamList} from '@libs/Navigation/types'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import {Account} from '@src/types/onyx'; -const propTypes = { - /** The parameters needed to authenticate with a short-lived token are in the URL */ - route: PropTypes.shape({ - /** Each parameter passed via the URL */ - params: PropTypes.shape({ - /** Short-lived authToken to sign in a user */ - shortLivedAuthToken: PropTypes.string, - - /** Short-lived authToken to sign in as a user, if they are coming from the old mobile app */ - shortLivedToken: PropTypes.string, - - /** The email of the transitioning user */ - email: PropTypes.string, - }), - }).isRequired, - +type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ - account: PropTypes.shape({ - /** Whether a sign is loading */ - isLoading: PropTypes.bool, - }), + account: OnyxEntry; }; -const defaultProps = { - account: { - isLoading: false, - }, -}; +type LogInWithShortLivedAuthTokenPageProps = LogInWithShortLivedAuthTokenPageOnyxProps & StackScreenProps; -function LogInWithShortLivedAuthTokenPage(props) { +function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedAuthTokenPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); + const { + params: {email, shortLivedAuthToken, shortLivedToken, exitTo, error}, + } = route; useEffect(() => { - const email = lodashGet(props, 'route.params.email', ''); - // We have to check for both shortLivedAuthToken and shortLivedToken, as the old mobile app uses shortLivedToken, and is not being actively updated. - const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '') || lodashGet(props, 'route.params.shortLivedToken', ''); + const token = shortLivedAuthToken || shortLivedToken; // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts - if (shortLivedAuthToken && !props.account.isLoading) { - Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken); + if (token && !account?.isLoading) { + Session.signInWithShortLivedAuthToken(email, token); return; } // If an error is returned as part of the route, ensure we set it in the onyxData for the account - const error = lodashGet(props, 'route.params.error', ''); if (error) { Session.setAccountError(error); } - const exitTo = lodashGet(props, 'route.params.exitTo', ''); if (exitTo) { Navigation.isNavigationReady().then(() => { Navigation.navigate(exitTo); @@ -76,9 +55,9 @@ function LogInWithShortLivedAuthTokenPage(props) { } // The only dependencies of the effect are based on props.route // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.route]); + }, [route]); - if (props.account.isLoading) { + if (account?.isLoading) { return ; } @@ -94,7 +73,7 @@ function LogInWithShortLivedAuthTokenPage(props) { {translate('deeplinkWrapper.launching')} - + {translate('deeplinkWrapper.expired')}{' '} { @@ -119,10 +98,8 @@ function LogInWithShortLivedAuthTokenPage(props) { ); } -LogInWithShortLivedAuthTokenPage.propTypes = propTypes; -LogInWithShortLivedAuthTokenPage.defaultProps = defaultProps; LogInWithShortLivedAuthTokenPage.displayName = 'LogInWithShortLivedAuthTokenPage'; -export default withOnyx({ +export default withOnyx({ account: {key: ONYXKEYS.ACCOUNT}, })(LogInWithShortLivedAuthTokenPage); From 7052b2212499ff4c689207fd482c0c09fac044f3 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 14 Dec 2023 15:00:00 +0000 Subject: [PATCH 005/159] refactor(typescript): apply pull request feedback --- src/libs/Navigation/types.ts | 11 +++++------ src/pages/LogInWithShortLivedAuthTokenPage.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 1a7f017cf4d8..84e838cad4ed 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -365,12 +365,11 @@ type RightModalNavigatorParamList = { type PublicScreensParamList = { [SCREENS.HOME]: undefined; [SCREENS.TRANSITION_BETWEEN_APPS]: { - shouldForceLogin: string; - email: string; - error: string; - shortLivedAuthToken: string; - shortLivedToken: string; - exitTo: Routes; + email?: string; + error?: string; + shortLivedAuthToken?: string; + shortLivedToken?: string; + exitTo?: Routes; }; [SCREENS.VALIDATE_LOGIN]: { accountID: string; diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index c262b4d14658..bee93f9db915 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -10,13 +10,13 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import {PublicScreensParamList} from '@libs/Navigation/types'; +import type {PublicScreensParamList} from '@libs/Navigation/types'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; -import {Account} from '@src/types/onyx'; +import type {Account} from '@src/types/onyx'; type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ @@ -35,10 +35,10 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA useEffect(() => { // We have to check for both shortLivedAuthToken and shortLivedToken, as the old mobile app uses shortLivedToken, and is not being actively updated. - const token = shortLivedAuthToken || shortLivedToken; + const token = shortLivedAuthToken ?? shortLivedToken; // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts - if (token && !account?.isLoading) { + if (email && token && !account?.isLoading) { Session.signInWithShortLivedAuthToken(email, token); return; } @@ -73,7 +73,7 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA {translate('deeplinkWrapper.launching')} - + {translate('deeplinkWrapper.expired')}{' '} { From 356a876806cebd308ede65c69cc00ffcdf6f763e Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 14 Dec 2023 15:11:02 +0000 Subject: [PATCH 006/159] refactor(typescript): add missing screen params types --- src/pages/ConciergePage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/ConciergePage.tsx b/src/pages/ConciergePage.tsx index ffb82f689620..d440e6a83705 100644 --- a/src/pages/ConciergePage.tsx +++ b/src/pages/ConciergePage.tsx @@ -1,11 +1,14 @@ import {useFocusEffect} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; +import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import {Session} from '@src/types/onyx'; type ConciergePageOnyxProps = { @@ -13,7 +16,7 @@ type ConciergePageOnyxProps = { session: OnyxEntry; }; -type ConciergePageProps = ConciergePageOnyxProps; +type ConciergePageProps = ConciergePageOnyxProps & StackScreenProps; /* * This is a "utility page", that does this: From fb473d0f2c7628a0dcc5b6fbeca8a5be59bbf856 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 18 Dec 2023 17:31:09 +0000 Subject: [PATCH 007/159] refactor: apply pull request feedback --- src/pages/ConciergePage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/ConciergePage.tsx b/src/pages/ConciergePage.tsx index d440e6a83705..5f4b5ad375f7 100644 --- a/src/pages/ConciergePage.tsx +++ b/src/pages/ConciergePage.tsx @@ -9,7 +9,7 @@ import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import {Session} from '@src/types/onyx'; +import type {Session} from '@src/types/onyx'; type ConciergePageOnyxProps = { /** Session info for the currently logged in user. */ @@ -38,6 +38,7 @@ function ConciergePage({session}: ConciergePageProps) { return ; } + ConciergePage.displayName = 'ConciergePage'; export default withOnyx({ From 7a8e43e3f1bf0be2e2a1b4827b573d42e02b26ce Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 18 Dec 2023 18:18:48 +0000 Subject: [PATCH 008/159] refactor: apply pull request feedback --- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index 2bf59bd76af7..5e95f31d1d6c 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -29,16 +29,14 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const { - params: {email, shortLivedAuthToken, shortLivedToken, exitTo, error}, - } = route; + const {email = '', shortLivedAuthToken = '', shortLivedToken = '', exitTo, error} = route?.params ?? {}; useEffect(() => { // We have to check for both shortLivedAuthToken and shortLivedToken, as the old mobile app uses shortLivedToken, and is not being actively updated. - const token = shortLivedAuthToken ?? shortLivedToken; + const token = shortLivedAuthToken || shortLivedToken; // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts - if (email && token && !account?.isLoading) { + if (token && !account?.isLoading) { Session.signInWithShortLivedAuthToken(email, token); return; } From e30aef2cef323c939996368409d8a6db404e5c56 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 20 Dec 2023 17:30:52 +0700 Subject: [PATCH 009/159] fix: plaid iframe transparent background --- web/index.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/index.html b/web/index.html index 967873fe586c..7c02614d17b2 100644 --- a/web/index.html +++ b/web/index.html @@ -96,6 +96,11 @@ caret-color: #ffffff; } + /* Customize Plaid iframe */ + [id^="plaid-link-iframe"] { + color-scheme: dark !important; + } + /* Prevent autofill from overlapping with the input label in Chrome */ div:has(input:-webkit-autofill, input[chrome-autofilled]) > label { transform: translateY(var(--active-label-translate-y)) scale(var(--active-label-scale)) !important; From 8810934caddf3cb2ba995a6bb5307dd9741585b7 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 21 Dec 2023 09:45:53 +0500 Subject: [PATCH 010/159] feat: add report field menu items --- src/components/OfflineWithFeedback.tsx | 2 +- .../ReportActionItem/MoneyReportView.tsx | 37 +++++++++++++++++-- src/libs/ReportUtils.ts | 18 ++++++++- src/pages/home/report/ReportActionItem.js | 9 ++++- src/pages/reportPropTypes.js | 3 ++ 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..4522595826af 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,7 +18,7 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction: OnyxCommon.PendingAction; + pendingAction?: OnyxCommon.PendingAction; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 36daa037fd78..2ffbda4d347b 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import {StyleProp, TextStyle, View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import SpacerView from '@components/SpacerView'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -13,22 +15,27 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; -import type {Report} from '@src/types/onyx'; +import type {PolicyReportField, Report} from '@src/types/onyx'; +import usePermissions from '@hooks/usePermissions'; type MoneyReportViewProps = { /** The report currently being looked at */ report: Report; + /** Policy report fields */ + policyReportFields: PolicyReportField[]; + /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; }; -function MoneyReportView({report, shouldShowHorizontalRule}: MoneyReportViewProps) { +function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); + const {canUseReportFields} = usePermissions(); const isSettled = ReportUtils.isSettled(report.reportID); const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); @@ -45,10 +52,34 @@ function MoneyReportView({report, shouldShowHorizontalRule}: MoneyReportViewProp StyleUtils.getColorStyle(theme.textSupporting), ]; + const sortedPolicyReportFields = useMemo(() => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight), [policyReportFields]); + return ( + {canUseReportFields && sortedPolicyReportFields.map((reportField) => { + const title = ReportUtils.getReportFieldTitle(report, reportField); + return ( + + {}} + shouldShowRightIcon + disabled={false} + wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} + shouldGreyOutWhenDisabled={false} + numberOfLinesTitle={0} + interactive + shouldStackHorizontally={false} + onSecondaryInteraction={() => {}} + hoverAndPressStyle={false} + titleWithTooltips={[]} + /> + + ); + })} , reportField: PolicyReportField) { + const value = report?.reportFields?.[reportField.fieldID] ?? reportField.defaultValue; + + if (reportField.type !== 'formula') { + return value; + } + + return value.replaceAll(/{report:([a-zA-Z]+)}/g, (match, property) => { + if (report && property in report) { + return report[property as keyof Report]?.toString() ?? match; + } + return match; + }); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4343,6 +4358,7 @@ export { canEditWriteCapability, hasSmartscanError, shouldAutoFocusOnKeyPress, + getReportFieldTitle, }; export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport}; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index c81e47016dcc..84e573145b9a 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -620,6 +620,7 @@ function ReportActionItem(props) { @@ -765,6 +766,10 @@ export default compose( }, initialValue: {}, }, + policyReportFields: { + key: ({report}) => report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined, + initialValue: [], + }, emojiReactions: { key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, initialValue: {}, @@ -800,6 +805,8 @@ export default compose( prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && - prevProps.linkedReportActionID === nextProps.linkedReportActionID, + prevProps.linkedReportActionID === nextProps.linkedReportActionID && + _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) && + _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields), ), ); diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index c89ea761f582..507965266ecc 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -67,4 +67,7 @@ export default PropTypes.shape({ /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.string), + + /** Custom fields attached to the report */ + reportFields: PropTypes.objectOf(PropTypes.string), }); From 7b67f02ec496bce8075bb50f5bf703fc5daa12ac Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 28 Dec 2023 17:23:47 +0100 Subject: [PATCH 011/159] Change ContextMenu extenstions --- .../BaseReportActionContextMenu.js | 204 ----------------- .../BaseReportActionContextMenu.tsx | 208 ++++++++++++++++++ ...tMenuActions.js => ContextMenuActions.tsx} | 9 - .../{index.native.js => index.native.tsx} | 0 .../{index.js => index.tsx} | 0 ....js => PopoverReportActionContextMenu.tsx} | 0 ...extMenu.js => ReportActionContextMenu.tsx} | 0 ...enericReportActionContextMenuPropTypes.ts} | 0 src/pages/home/report/ContextMenu/types.ts | 33 +++ 9 files changed, 241 insertions(+), 213 deletions(-) delete mode 100755 src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js create mode 100755 src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx rename src/pages/home/report/ContextMenu/{ContextMenuActions.js => ContextMenuActions.tsx} (99%) rename src/pages/home/report/ContextMenu/MiniReportActionContextMenu/{index.native.js => index.native.tsx} (100%) rename src/pages/home/report/ContextMenu/MiniReportActionContextMenu/{index.js => index.tsx} (100%) rename src/pages/home/report/ContextMenu/{PopoverReportActionContextMenu.js => PopoverReportActionContextMenu.tsx} (100%) rename src/pages/home/report/ContextMenu/{ReportActionContextMenu.js => ReportActionContextMenu.tsx} (100%) rename src/pages/home/report/ContextMenu/{genericReportActionContextMenuPropTypes.js => genericReportActionContextMenuPropTypes.ts} (100%) create mode 100644 src/pages/home/report/ContextMenu/types.ts diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js deleted file mode 100755 index 33adfa4b35f9..000000000000 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js +++ /dev/null @@ -1,204 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {memo, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import ContextMenuItem from '@components/ContextMenuItem'; -import {withBetas} from '@components/OnyxProvider'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; -import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; -import useNetwork from '@hooks/useNetwork'; -import compose from '@libs/compose'; -import useStyleUtils from '@styles/useStyleUtils'; -import * as Session from '@userActions/Session'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ContextMenuActions, {CONTEXT_MENU_TYPES} from './ContextMenuActions'; -import {defaultProps as GenericReportActionContextMenuDefaultProps, propTypes as genericReportActionContextMenuPropTypes} from './genericReportActionContextMenuPropTypes'; -import {hideContextMenu} from './ReportActionContextMenu'; - -const propTypes = { - /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ - type: PropTypes.string, - - /** Target node which is the target of ContentMenu */ - anchor: PropTypes.oneOfType([PropTypes.node, PropTypes.object]), - - /** Flag to check if the chat participant is Chronos */ - isChronosReport: PropTypes.bool, - - /** Whether the provided report is an archived room */ - isArchivedRoom: PropTypes.bool, - - contentRef: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.func]), - - ...genericReportActionContextMenuPropTypes, - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - type: CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor: null, - contentRef: null, - isChronosReport: false, - isArchivedRoom: false, - ...GenericReportActionContextMenuDefaultProps, -}; -function BaseReportActionContextMenu(props) { - const StyleUtils = useStyleUtils(); - const menuItemRefs = useRef({}); - const [shouldKeepOpen, setShouldKeepOpen] = useState(false); - const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(props.isMini, props.isSmallScreenWidth); - const {isOffline} = useNetwork(); - - const reportAction = useMemo(() => { - if (_.isEmpty(props.reportActions) || props.reportActionID === '0') { - return {}; - } - return props.reportActions[props.reportActionID] || {}; - }, [props.reportActions, props.reportActionID]); - - const shouldShowFilter = (contextAction) => - contextAction.shouldShow( - props.type, - reportAction, - props.isArchivedRoom, - props.betas, - props.anchor, - props.isChronosReport, - props.reportID, - props.isPinnedChat, - props.isUnreadChat, - isOffline, - ); - - const shouldEnableArrowNavigation = !props.isMini && (props.isVisible || shouldKeepOpen); - const filteredContextMenuActions = _.filter(ContextMenuActions, shouldShowFilter); - - // Context menu actions that are not rendered as menu items are excluded from arrow navigation - const nonMenuItemActionIndexes = _.map(filteredContextMenuActions, (contextAction, index) => (_.isFunction(contextAction.renderContent) ? index : undefined)); - const disabledIndexes = _.filter(nonMenuItemActionIndexes, (index) => !_.isUndefined(index)); - - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ - initialFocusedIndex: -1, - disabledIndexes, - maxIndex: filteredContextMenuActions.length - 1, - isActive: shouldEnableArrowNavigation, - }); - - /** - * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and - * shows the sign in modal. Else, executes the callback. - * - * @param {Function} callback - * @param {Boolean} isAnonymousAction - */ - const interceptAnonymousUser = (callback, isAnonymousAction = false) => { - if (Session.isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - - InteractionManager.runAfterInteractions(() => { - Session.signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - - useKeyboardShortcut( - CONST.KEYBOARD_SHORTCUTS.ENTER, - (event) => { - if (!menuItemRefs.current[focusedIndex]) { - return; - } - - // Ensures the event does not cause side-effects beyond the context menu, e.g. when an outside element is focused - if (event) { - event.stopPropagation(); - } - - menuItemRefs.current[focusedIndex].triggerPressAndUpdateSuccess(); - setFocusedIndex(-1); - }, - {isActive: shouldEnableArrowNavigation}, - ); - - return ( - (props.isVisible || shouldKeepOpen) && ( - - {_.map(filteredContextMenuActions, (contextAction, index) => { - const closePopup = !props.isMini; - const payload = { - reportAction, - reportID: props.reportID, - draftMessage: props.draftMessage, - selection: props.selection, - close: () => setShouldKeepOpen(false), - openContextMenu: () => setShouldKeepOpen(true), - interceptAnonymousUser, - }; - - if (contextAction.renderContent) { - // make sure that renderContent isn't mixed with unsupported props - if (__DEV__ && (contextAction.text != null || contextAction.icon != null)) { - throw new Error('Dev error: renderContent() and text/icon cannot be used together.'); - } - - return contextAction.renderContent(closePopup, payload); - } - - return ( - { - menuItemRefs.current[index] = ref; - }} - icon={contextAction.icon} - text={props.translate(contextAction.textTranslateKey, {action: reportAction})} - successIcon={contextAction.successIcon} - successText={contextAction.successTextTranslateKey ? props.translate(contextAction.successTextTranslateKey) : undefined} - isMini={props.isMini} - key={contextAction.textTranslateKey} - onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)} - description={contextAction.getDescription(props.selection, props.isSmallScreenWidth)} - isAnonymousAction={contextAction.isAnonymousAction} - isFocused={focusedIndex === index} - /> - ); - })} - - ) - ); -} - -BaseReportActionContextMenu.propTypes = propTypes; -BaseReportActionContextMenu.defaultProps = defaultProps; - -export default compose( - withLocalize, - withBetas(), - withWindowDimensions, - withOnyx({ - reportActions: { - key: ({originalReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, - canEvict: false, - }, - }), -)( - memo(BaseReportActionContextMenu, (prevProps, nextProps) => { - const prevReportAction = lodashGet(prevProps.reportActions, prevProps.reportActionID, ''); - const nextReportAction = lodashGet(nextProps.reportActions, nextProps.reportActionID, ''); - - // We only want to re-render when the report action that is attached to is changed - if (prevReportAction !== nextReportAction) { - return false; - } - return _.isEqual(_.omit(prevProps, 'reportActions'), _.omit(nextProps, 'reportActions')); - }), -); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx new file mode 100755 index 000000000000..3639c9349549 --- /dev/null +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -0,0 +1,208 @@ +import lodashIsEqual from 'lodash/isEqual'; +import React, {memo, useMemo, useRef, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import ContextMenuItem from '@components/ContextMenuItem'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import useStyleUtils from '@styles/useStyleUtils'; +import * as Session from '@userActions/Session'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Beta, ReportActions} from '@src/types/onyx'; +import ContextMenuActions from './ContextMenuActions'; +import {hideContextMenu} from './ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES, GenericReportActionContextMenuProps} from './types'; + +type BaseReportActionContextMenuOnyxProps = { + /** Beta features list */ + betas: OnyxEntry; + + /** All of the actions of the report */ + reportActions: OnyxEntry; +}; + +type BaseReportActionContextMenuProps = GenericReportActionContextMenuProps & + BaseReportActionContextMenuOnyxProps & { + /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ + type?: ValueOf; + + /** Target node which is the target of ContentMenu */ + anchor: any; + + /** Flag to check if the chat participant is Chronos */ + isChronosReport: boolean; + + /** Whether the provided report is an archived room */ + isArchivedRoom: boolean; + + /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */ + isPinnedChat?: boolean; + + /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ + isUnreadChat?: boolean; + + /** Content Ref */ + contentRef: any; + }; + +function BaseReportActionContextMenu({ + type = CONTEXT_MENU_TYPES.REPORT_ACTION, + anchor = null, + contentRef = null, + isChronosReport = false, + isArchivedRoom = false, + isMini = false, + isVisible = false, + isPinnedChat = false, + isUnreadChat = false, + selection = '', + draftMessage = '', + reportActionID, + reportID, + betas, + reportActions, +}: BaseReportActionContextMenuProps) { + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const menuItemRefs = useRef void}>>({}); + const [shouldKeepOpen, setShouldKeepOpen] = useState(false); + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, isSmallScreenWidth); + const {isOffline} = useNetwork(); + + const reportAction = useMemo(() => { + if (_.isEmpty(reportActions) || reportActionID === '0') { + return {}; + } + return reportActions[reportActionID] || {}; + }, [reportActions, reportActionID]); + + const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); + const filteredContextMenuActions = ContextMenuActions.filter((contextAction) => + contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline), + ); + + // Context menu actions that are not rendered as menu items are excluded from arrow navigation + const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) => (typeof contextAction.renderContent === 'function' ? index : undefined)); + const disabledIndexes = nonMenuItemActionIndexes.filter((index): index is number => index !== undefined); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes, + maxIndex: filteredContextMenuActions.length - 1, + isActive: shouldEnableArrowNavigation, + }); + + /** + * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and + * shows the sign in modal. Else, executes the callback. + */ + const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { + if (Session.isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + + InteractionManager.runAfterInteractions(() => { + Session.signOutAndRedirectToSignIn(); + }); + } else { + callback(); + } + }; + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.ENTER, + (event) => { + if (!menuItemRefs.current[focusedIndex]) { + return; + } + + // Ensures the event does not cause side-effects beyond the context menu, e.g. when an outside element is focused + if (event) { + event.stopPropagation(); + } + + menuItemRefs.current[focusedIndex].triggerPressAndUpdateSuccess(); + setFocusedIndex(-1); + }, + {isActive: shouldEnableArrowNavigation}, + ); + + return ( + (isVisible || shouldKeepOpen) && ( + + {filteredContextMenuActions.map((contextAction, index) => { + const closePopup = !isMini; + const payload = { + reportAction, + reportID, + draftMessage, + selection, + close: () => setShouldKeepOpen(false), + openContextMenu: () => setShouldKeepOpen(true), + interceptAnonymousUser, + }; + + if (contextAction.renderContent) { + // make sure that renderContent isn't mixed with unsupported props + if (__DEV__ && (contextAction.text != null || contextAction.icon != null)) { + throw new Error('Dev error: renderContent() and text/icon cannot be used together.'); + } + + return contextAction.renderContent(closePopup, payload); + } + + return ( + { + menuItemRefs.current[index] = ref; + }} + icon={contextAction.icon} + text={translate(contextAction.textTranslateKey, {action: reportAction})} + successIcon={contextAction.successIcon} + successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} + isMini={isMini} + key={contextAction.textTranslateKey} + onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)} + description={contextAction.getDescription(selection, isSmallScreenWidth)} + isAnonymousAction={contextAction.isAnonymousAction} + isFocused={focusedIndex === index} + /> + ); + })} + + ) + ); +} + +export default withOnyx({ + betas: { + key: ONYXKEYS.BETAS, + }, + reportActions: { + key: ({originalReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, + canEvict: false, + }, +})( + memo(BaseReportActionContextMenu, (prevProps, nextProps) => { + const {reportActions: prevReportActions, ...prevPropsWithoutReportActions} = prevProps; + const {reportActions: nextReportActions, ...nextPropsWithoutReportActions} = nextProps; + + const prevReportAction = prevReportActions?.[prevProps.reportActionID] ?? ''; + const nextReportAction = nextReportActions?.[nextProps.reportActionID] ?? ''; + + // We only want to re-render when the report action that is attached to is changed + if (prevReportAction !== nextReportAction) { + return false; + } + + return lodashIsEqual(prevPropsWithoutReportActions, nextPropsWithoutReportActions); + }), +); diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx similarity index 99% rename from src/pages/home/report/ContextMenu/ContextMenuActions.js rename to src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 2d9faa574ebb..dbd63516c525 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -34,13 +34,6 @@ function getActionText(reportAction) { return lodashGet(message, 'html', ''); } -const CONTEXT_MENU_TYPES = { - LINK: 'LINK', - REPORT_ACTION: 'REPORT_ACTION', - EMAIL: 'EMAIL', - REPORT: 'REPORT', -}; - // A list of all the context actions in this menu. export default [ { @@ -468,5 +461,3 @@ export default [ getDescription: () => {}, }, ]; - -export {CONTEXT_MENU_TYPES}; diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.js rename to src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js rename to src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js rename to src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js b/src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/ReportActionContextMenu.js rename to src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx diff --git a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts similarity index 100% rename from src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js rename to src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts diff --git a/src/pages/home/report/ContextMenu/types.ts b/src/pages/home/report/ContextMenu/types.ts new file mode 100644 index 000000000000..051e7635bc80 --- /dev/null +++ b/src/pages/home/report/ContextMenu/types.ts @@ -0,0 +1,33 @@ +const CONTEXT_MENU_TYPES = { + LINK: 'LINK', + REPORT_ACTION: 'REPORT_ACTION', + EMAIL: 'EMAIL', + REPORT: 'REPORT', +} as const; + +type GenericReportActionContextMenuProps = { + /** The ID of the report this report action is attached to. */ + reportID: string; + + /** The ID of the report action this context menu is attached to. */ + reportActionID: string; + + /** The ID of the original report from which the given reportAction is first created. */ + originalReportID: string; + + /** If true, this component will be a small, row-oriented menu that displays icons but not text. + If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. */ + isMini?: boolean; + + /** Controls the visibility of this component. */ + isVisible?: boolean; + + /** The copy selection. */ + selection?: string; + + /** Draft message - if this is set the comment is in 'edit' mode */ + draftMessage?: string; +}; + +export {CONTEXT_MENU_TYPES}; +export type {GenericReportActionContextMenuProps}; From 44abb622fb39576c25a3bf12fd3ef0beb5c18777 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 28 Dec 2023 17:43:43 +0100 Subject: [PATCH 012/159] Migrate MiniReportActionContextMenu --- .../BaseReportActionContextMenu.tsx | 59 +++++--- .../index.native.tsx | 5 +- .../MiniReportActionContextMenu/index.tsx | 30 +--- .../MiniReportActionContextMenu/types.ts | 8 + .../ContextMenu/ReportActionContextMenu.tsx | 138 ------------------ ...genericReportActionContextMenuPropTypes.ts | 34 ----- src/pages/home/report/ContextMenu/types.ts | 8 - 7 files changed, 58 insertions(+), 224 deletions(-) create mode 100644 src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts delete mode 100644 src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx delete mode 100644 src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 20012c15ef30..08d1e7a0c28a 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -16,7 +16,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, ReportActions} from '@src/types/onyx'; import ContextMenuActions from './ContextMenuActions'; import {hideContextMenu} from './ReportActionContextMenu'; -import {GenericReportActionContextMenuProps} from './types'; type BaseReportActionContextMenuOnyxProps = { /** Beta features list */ @@ -26,29 +25,51 @@ type BaseReportActionContextMenuOnyxProps = { reportActions: OnyxEntry; }; -type BaseReportActionContextMenuProps = GenericReportActionContextMenuProps & - BaseReportActionContextMenuOnyxProps & { - /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ - type?: ValueOf; +type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { + /** The ID of the report this report action is attached to. */ + reportID: string; - /** Target node which is the target of ContentMenu */ - anchor: any; + /** The ID of the report action this context menu is attached to. */ + reportActionID: string; - /** Flag to check if the chat participant is Chronos */ - isChronosReport: boolean; + /** The ID of the original report from which the given reportAction is first created. */ + // eslint-disable-next-line react/no-unused-prop-types + originalReportID: string; - /** Whether the provided report is an archived room */ - isArchivedRoom: boolean; + /** If true, this component will be a small, row-oriented menu that displays icons but not text. + If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. */ + isMini?: boolean; - /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */ - isPinnedChat?: boolean; + /** Controls the visibility of this component. */ + isVisible?: boolean; - /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ - isUnreadChat?: boolean; + /** The copy selection. */ + selection?: string; - /** Content Ref */ - contentRef: any; - }; + /** Draft message - if this is set the comment is in 'edit' mode */ + draftMessage?: string; + + /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ + type?: ValueOf; + + /** Target node which is the target of ContentMenu */ + anchor: any; + + /** Flag to check if the chat participant is Chronos */ + isChronosReport: boolean; + + /** Whether the provided report is an archived room */ + isArchivedRoom: boolean; + + /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */ + isPinnedChat?: boolean; + + /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ + isUnreadChat?: boolean; + + /** Content Ref */ + contentRef: any; +}; function BaseReportActionContextMenu({ type = CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, @@ -206,3 +227,5 @@ export default withOnyx null; +import MiniReportActionContextMenuProps from './types'; + +// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars +export default (props: MiniReportActionContextMenuProps) => null; diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx index d858206cdfc3..fbc6b90a3424 100644 --- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,47 +1,27 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import useStyleUtils from '@hooks/useStyleUtils'; import BaseReportActionContextMenu from '@pages/home/report/ContextMenu/BaseReportActionContextMenu'; -import { - defaultProps as GenericReportActionContextMenuDefaultProps, - propTypes as genericReportActionContextMenuPropTypes, -} from '@pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes'; import CONST from '@src/CONST'; +import MiniReportActionContextMenuProps from './types'; -const propTypes = { - ..._.omit(genericReportActionContextMenuPropTypes, ['isMini']), - - /** Should the reportAction this menu is attached to have the appearance of being - * grouped with the previous reportAction? */ - displayAsGroup: PropTypes.bool, -}; - -const defaultProps = { - ..._.omit(GenericReportActionContextMenuDefaultProps, ['isMini']), - displayAsGroup: false, -}; - -function MiniReportActionContextMenu(props) { +function MiniReportActionContextMenu({displayAsGroup = false, ...rest}: MiniReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); return ( ); } -MiniReportActionContextMenu.propTypes = propTypes; -MiniReportActionContextMenu.defaultProps = defaultProps; MiniReportActionContextMenu.displayName = 'MiniReportActionContextMenu'; export default MiniReportActionContextMenu; diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts new file mode 100644 index 000000000000..d28d70180819 --- /dev/null +++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts @@ -0,0 +1,8 @@ +import {BaseReportActionContextMenuProps} from '@pages/home/report/ContextMenu/BaseReportActionContextMenu'; + +type MiniReportActionContextMenuProps = BaseReportActionContextMenuProps & { + /** Should the reportAction this menu is attached to have the appearance of being grouped with the previous reportAction? */ + displayAsGroup?: boolean; +}; + +export default MiniReportActionContextMenuProps; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx deleted file mode 100644 index 9467ff19b2f5..000000000000 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react'; - -const contextMenuRef = React.createRef(); - -/** - * Hide the ReportActionContextMenu modal popover. - * Hides the popover menu with an optional delay - * @param {Boolean} shouldDelay - whether the menu should close after a delay - * @param {Function} [onHideCallback=() => {}] - Callback to be called after Context Menu is completely hidden - */ -function hideContextMenu(shouldDelay, onHideCallback = () => {}) { - if (!contextMenuRef.current) { - return; - } - if (!shouldDelay) { - contextMenuRef.current.hideContextMenu(onHideCallback); - - return; - } - - // Save the active instanceID for which hide action was called. - // If menu is being closed with a delay, check that whether the same instance exists or a new was created. - // If instance is not same, cancel the hide action - const instanceID = contextMenuRef.current.instanceID; - setTimeout(() => { - if (contextMenuRef.current.instanceID !== instanceID) { - return; - } - - contextMenuRef.current.hideContextMenu(onHideCallback); - }, 800); -} - -/** - * Show the ReportActionContextMenu modal popover. - * - * @param {string} type - the context menu type to display [EMAIL, LINK, REPORT_ACTION, REPORT] - * @param {Object} [event] - A press event. - * @param {String} [selection] - Copied content. - * @param {Element} contextMenuAnchor - popoverAnchor - * @param {String} reportID - Active Report Id - * @param {String} reportActionID - ReportActionID for ContextMenu - * @param {String} originalReportID - The currrent Report Id of the reportAction - * @param {String} draftMessage - ReportAction Draftmessage - * @param {Function} [onShow=() => {}] - Run a callback when Menu is shown - * @param {Function} [onHide=() => {}] - Run a callback when Menu is hidden - * @param {Boolean} isArchivedRoom - Whether the provided report is an archived room - * @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos - * @param {Boolean} isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action - * @param {Boolean} isUnreadChat - Flag to check if the chat has unread messages in the LHN. Used for the Mark as Read/Unread action - */ -function showContextMenu( - type, - event, - selection, - contextMenuAnchor, - reportID = '0', - reportActionID = '0', - originalReportID = '0', - draftMessage = '', - onShow = () => {}, - onHide = () => {}, - isArchivedRoom = false, - isChronosReport = false, - isPinnedChat = false, - isUnreadChat = false, -) { - if (!contextMenuRef.current) { - return; - } - // If there is an already open context menu, close it first before opening - // a new one. - if (contextMenuRef.current.instanceID) { - hideContextMenu(); - contextMenuRef.current.runAndResetOnPopoverHide(); - } - - contextMenuRef.current.showContextMenu( - type, - event, - selection, - contextMenuAnchor, - reportID, - reportActionID, - originalReportID, - draftMessage, - onShow, - onHide, - isArchivedRoom, - isChronosReport, - isPinnedChat, - isUnreadChat, - ); -} - -function hideDeleteModal() { - if (!contextMenuRef.current) { - return; - } - contextMenuRef.current.hideDeleteModal(); -} - -/** - * Opens the Confirm delete action modal - * @param {String} reportID - * @param {Object} reportAction - * @param {Boolean} [shouldSetModalVisibility] - * @param {Function} [onConfirm] - * @param {Function} [onCancel] - */ -function showDeleteModal(reportID, reportAction, shouldSetModalVisibility, onConfirm, onCancel) { - if (!contextMenuRef.current) { - return; - } - contextMenuRef.current.showDeleteModal(reportID, reportAction, shouldSetModalVisibility, onConfirm, onCancel); -} - -/** - * Whether Context Menu is active for the Report Action. - * - * @param {Number|String} actionID - * @return {Boolean} - */ -function isActiveReportAction(actionID) { - if (!contextMenuRef.current) { - return; - } - return contextMenuRef.current.isActiveReportAction(actionID); -} - -function clearActiveReportAction() { - if (!contextMenuRef.current) { - return; - } - return contextMenuRef.current.clearActiveReportAction(); -} - -export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, clearActiveReportAction, showDeleteModal, hideDeleteModal}; diff --git a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts deleted file mode 100644 index 3d8667e44e62..000000000000 --- a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** The ID of the report this report action is attached to. */ - reportID: PropTypes.string.isRequired, - - /** The ID of the report action this context menu is attached to. */ - reportActionID: PropTypes.string.isRequired, - - /** The ID of the original report from which the given reportAction is first created. */ - originalReportID: PropTypes.string.isRequired, - - /** If true, this component will be a small, row-oriented menu that displays icons but not text. - If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. */ - isMini: PropTypes.bool, - - /** Controls the visibility of this component. */ - isVisible: PropTypes.bool, - - /** The copy selection. */ - selection: PropTypes.string, - - /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage: PropTypes.string, -}; - -const defaultProps = { - isMini: false, - isVisible: false, - selection: '', - draftMessage: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/pages/home/report/ContextMenu/types.ts b/src/pages/home/report/ContextMenu/types.ts index 051e7635bc80..76af0d1e3f13 100644 --- a/src/pages/home/report/ContextMenu/types.ts +++ b/src/pages/home/report/ContextMenu/types.ts @@ -1,10 +1,3 @@ -const CONTEXT_MENU_TYPES = { - LINK: 'LINK', - REPORT_ACTION: 'REPORT_ACTION', - EMAIL: 'EMAIL', - REPORT: 'REPORT', -} as const; - type GenericReportActionContextMenuProps = { /** The ID of the report this report action is attached to. */ reportID: string; @@ -29,5 +22,4 @@ type GenericReportActionContextMenuProps = { draftMessage?: string; }; -export {CONTEXT_MENU_TYPES}; export type {GenericReportActionContextMenuProps}; From 84e77cd4ffb0b18ecc4ddf17c9d4798a3b8f14b1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Dec 2023 14:31:52 +0100 Subject: [PATCH 013/159] Migrate ContextMenuActions --- .../report/ContextMenu/ContextMenuActions.tsx | 132 +++++++++++------- 1 file changed, 83 insertions(+), 49 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 2f00f81e0fa5..ab28e741e6bc 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,7 +1,7 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import lodashGet from 'lodash/get'; import React from 'react'; -import _ from 'underscore'; +import {OnyxEntry} from 'react-native-onyx'; +import {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; @@ -22,24 +22,20 @@ import * as TaskUtils from '@libs/TaskUtils'; import * as Download from '@userActions/Download'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; +import {Beta, ReportAction, ReportActionReactions} from '@src/types/onyx'; +import IconAsset from '@src/types/utils/IconAsset'; import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; -/** - * Gets the HTML version of the message in an action. - * @param reportAction - * @return - */ -function getActionText(reportAction) { - const message = _.last(lodashGet(reportAction, 'message', null)); - return lodashGet(message, 'html', ''); +/** Gets the HTML version of the message in an action */ +function getActionText(reportAction: OnyxEntry) { + const message = reportAction?.message?.at(-1) ?? null; + return message?.html ?? ''; } -/** - * Sets the HTML string to Clipboard. - * @param content - */ -function setClipboardMessage(content) { +/** Sets the HTML string to Clipboard */ +function setClipboardMessage(content: string) { const parser = new ExpensiMark(); if (!Clipboard.canSetHtml()) { Clipboard.setString(parser.htmlToMarkdown(content)); @@ -49,16 +45,56 @@ function setClipboardMessage(content) { } } +type ShouldShow = ( + type: string, + reportAction: ReportAction, + isArchivedRoom: boolean, + betas: Beta[], + menuTarget: HTMLElement, + isChronosReport: boolean, + reportID: string, + isPinnedChat: boolean, + isUnreadChat: boolean, + anchor: string, +) => boolean; + +type Payload = { + reportAction: ReportAction; + reportID: string; + draftMessage: string; + selection: string; + close: () => void; + openContextMenu: () => void; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; +}; + +type OnPress = (closePopover: boolean, payload: Payload, selection: string | undefined, reportID: string, draftMessage: string) => void; + +type RenderContent = (closePopover: boolean, payload: Payload) => React.ReactElement; + +type GetDescription = (selection?: string) => string | void; + +type ContextMenuAction = { + isAnonymousAction: boolean; + shouldShow: ShouldShow; + textTranslateKey?: TranslationPaths; + successTextTranslateKey?: TranslationPaths; + icon?: IconAsset; + successIcon?: IconAsset; + renderContent?: RenderContent; + onPress?: OnPress; + getDescription?: GetDescription; +}; + // A list of all the context actions in this menu. -export default [ +const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, - shouldKeepOpen: true, - shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && _.has(reportAction, 'message') && !ReportActionsUtils.isMessageDeleted(reportAction), + shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => { const isMini = !closePopover; - const closeContextMenu = (onHideCallback) => { + const closeContextMenu = (onHideCallback?: () => void) => { if (isMini) { closeManually(); if (onHideCallback) { @@ -69,7 +105,7 @@ export default [ } }; - const toggleEmojiAndCloseMenu = (emoji, existingReactions) => { + const toggleEmojiAndCloseMenu = (emoji: Emoji, existingReactions: ReportActionReactions | undefined) => { Report.toggleEmojiReaction(reportID, reportAction, emoji, existingReactions); closeContextMenu(); }; @@ -78,6 +114,7 @@ export default [ return ( @@ -106,12 +144,13 @@ export default [ successIcon: Expensicons.Download, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); - const messageHtml = lodashGet(reportAction, ['message', 0, 'html']); - return isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline; + const messageHtml = getActionText; + return ( + isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline + ); }, onPress: (closePopover, {reportAction}) => { - const message = _.last(lodashGet(reportAction, 'message', [{}])); - const html = lodashGet(message, 'html', ''); + const html = getActionText(reportAction); const attachmentDetails = getAttachmentDetails(html); const {originalFileName, sourceURL} = attachmentDetails; const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL); @@ -128,8 +167,6 @@ export default [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.replyInThread', icon: Expensicons.ChatBubble, - successTextTranslateKey: '', - successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; @@ -150,12 +187,12 @@ export default [ if (closePopover) { hideContextMenu(false, () => { ReportActionComposeFocusManager.focus(); - Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); }); return; } - Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); }, getDescription: () => {}, }, @@ -163,10 +200,8 @@ export default [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.subscribeToThread', icon: Expensicons.Bell, - successTextTranslateKey: '', - successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -179,7 +214,7 @@ export default [ return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -187,13 +222,13 @@ export default [ if (closePopover) { hideContextMenu(false, () => { ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }); return; } ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }, getDescription: () => {}, }, @@ -201,10 +236,8 @@ export default [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread', icon: Expensicons.BellSlash, - successTextTranslateKey: '', - successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -219,7 +252,7 @@ export default [ return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -227,13 +260,13 @@ export default [ if (closePopover) { hideContextMenu(false, () => { ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }); return; } ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }, getDescription: () => {}, }, @@ -278,8 +311,7 @@ export default [ onPress: (closePopover, {reportAction, selection}) => { const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); - const message = _.last(lodashGet(reportAction, 'message', [{}])); - const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction.actionName) : lodashGet(message, 'html', ''); + const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction.actionName) : getActionText(reportAction); const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); if (!isAttachment) { @@ -298,11 +330,11 @@ export default [ const taskPreviewMessage = TaskUtils.getTaskCreatedMessage(reportAction); Clipboard.setString(taskPreviewMessage); } else if (ReportActionsUtils.isMemberChangeAction(reportAction)) { - const logMessage = ReportActionsUtils.getMemberChangeMessageFragment(reportAction).html; + const logMessage = ReportActionsUtils.getMemberChangeMessageFragment(reportAction).html ?? ''; setClipboardMessage(logMessage); } else if (ReportActionsUtils.isSubmittedExpenseAction(reportAction)) { - const submittedMessage = _.reduce(reportAction.message, (acc, curr) => `${acc}${curr.text}`, ''); - Clipboard.setString(submittedMessage); + const submittedMessage = reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, ''); + Clipboard.setString(submittedMessage ?? ''); } else if (content) { setClipboardMessage(content); } @@ -325,12 +357,12 @@ export default [ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); // Only hide the copylink menu item when context menu is opened over img element. - const isAttachmentTarget = lodashGet(menuTarget, 'tagName') === 'IMG' && isAttachment; + const isAttachmentTarget = menuTarget?.tagName === 'IMG' && isAttachment; return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction); }, onPress: (closePopover, {reportAction, reportID}) => { Environment.getEnvironmentURL().then((environmentURL) => { - const reportActionID = lodashGet(reportAction, 'reportActionID'); + const reportActionID = reportAction?.reportActionID; Clipboard.setString(`${environmentURL}/r/${reportID}/${reportActionID}`); }); hideContextMenu(true, ReportActionComposeFocusManager.focus); @@ -378,7 +410,7 @@ export default [ onPress: (closePopover, {reportID, reportAction, draftMessage}) => { if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { hideContextMenu(false); - const childReportID = lodashGet(reportAction, 'childReportID', 0); + const childReportID = reportAction?.childReportID ?? 0; if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); @@ -390,7 +422,7 @@ export default [ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } - const editAction = () => Report.saveReportActionDraft(reportID, reportAction, _.isEmpty(draftMessage) ? getActionText(reportAction) : ''); + const editAction = () => Report.saveReportActionDraft(reportID, reportAction, !draftMessage ? getActionText(reportAction) : ''); if (closePopover) { // Hide popover, then call editAction @@ -474,3 +506,5 @@ export default [ getDescription: () => {}, }, ]; + +export default ContextMenuActions; From d36fb5bbb1269d90d795f6113833dd888690b82c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Dec 2023 15:12:05 +0100 Subject: [PATCH 014/159] Fix ContextMenuActions type errors --- .../report/ContextMenu/ContextMenuActions.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index ab28e741e6bc..e0dbd6e94159 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -144,19 +144,18 @@ const ContextMenuActions: ContextMenuAction[] = [ successIcon: Expensicons.Download, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); - const messageHtml = getActionText; + const messageHtml = reportAction?.message?.at(0)?.html; return ( isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline ); }, onPress: (closePopover, {reportAction}) => { const html = getActionText(reportAction); - const attachmentDetails = getAttachmentDetails(html); - const {originalFileName, sourceURL} = attachmentDetails; - const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL); - const sourceID = (sourceURL.match(CONST.REGEX.ATTACHMENT_ID) || [])[1]; + const {originalFileName, sourceURL} = getAttachmentDetails(html); + const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? ''); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; Download.setDownload(sourceID, true); - fileDownload(sourceURLWithAuth, originalFileName).then(() => Download.setDownload(sourceID, false)); + fileDownload(sourceURLWithAuth, originalFileName ?? '').then(() => Download.setDownload(sourceID, false)); if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } @@ -214,7 +213,7 @@ const ContextMenuActions: ContextMenuAction[] = [ return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; + let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -237,7 +236,7 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread', icon: Expensicons.BellSlash, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { - let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; + let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -252,7 +251,7 @@ const ContextMenuActions: ContextMenuAction[] = [ return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; + let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -294,7 +293,7 @@ const ContextMenuActions: ContextMenuAction[] = [ Clipboard.setString(EmailUtils.trimMailTo(selection)); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, - getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection)), + getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')), }, { isAnonymousAction: true, @@ -413,7 +412,7 @@ const ContextMenuActions: ContextMenuAction[] = [ const childReportID = reportAction?.childReportID ?? 0; if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []); Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; @@ -493,7 +492,6 @@ const ContextMenuActions: ContextMenuAction[] = [ ReportUtils.canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && - !ReportUtils.isConciergeChatReport(reportID) && reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { From e395228009d7589dea28a288d5eb825805ff331d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Dec 2023 21:28:03 +0100 Subject: [PATCH 015/159] Improve ContextMenu types --- src/libs/actions/IOU.js | 2 +- .../BaseReportActionContextMenu.tsx | 24 ++--- .../report/ContextMenu/ContextMenuActions.tsx | 62 +++++------ .../PopoverReportActionContextMenu.tsx | 101 ++++++++++-------- .../ContextMenu/ReportActionContextMenu.ts | 1 + 5 files changed, 102 insertions(+), 88 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index d43fefca20bc..0cb3a3277b55 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2277,7 +2277,7 @@ function updateMoneyRequestAmountAndCurrency(transactionID, transactionThreadRep } /** - * @param {String} transactionID + * @param {String | undefined} transactionID * @param {Object} reportAction - the money request reportAction we are deleting * @param {Boolean} isSingleTransactionView */ diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 08d1e7a0c28a..96fc5ba1c944 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,8 +1,7 @@ import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, useMemo, useRef, useState} from 'react'; +import React, {memo, RefObject, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; import ContextMenuItem from '@components/ContextMenuItem'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; @@ -13,9 +12,10 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Beta, ReportActions} from '@src/types/onyx'; +import {Beta, ReportAction, ReportActions} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ContextMenuActions from './ContextMenuActions'; -import {hideContextMenu} from './ReportActionContextMenu'; +import {ContextMenuType, hideContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { /** Beta features list */ @@ -50,10 +50,10 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { draftMessage?: string; /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ - type?: ValueOf; + type?: ContextMenuType; /** Target node which is the target of ContentMenu */ - anchor: any; + anchor: HTMLElement; /** Flag to check if the chat participant is Chronos */ isChronosReport: boolean; @@ -68,7 +68,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { isUnreadChat?: boolean; /** Content Ref */ - contentRef: any; + contentRef?: RefObject; }; function BaseReportActionContextMenu({ @@ -96,16 +96,16 @@ function BaseReportActionContextMenu({ const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, isSmallScreenWidth); const {isOffline} = useNetwork(); - const reportAction = useMemo(() => { - if (_.isEmpty(reportActions) || reportActionID === '0') { - return {}; + const reportAction: OnyxEntry = useMemo(() => { + if (isEmptyObject(reportActions) || reportActionID === '0') { + return null; } - return reportActions[reportActionID] || {}; + return reportActions[reportActionID] ?? null; }, [reportActions, reportActionID]); const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); const filteredContextMenuActions = ContextMenuActions.filter((contextAction) => - contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline), + contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline), ); // Context menu actions that are not rendered as menu items are excluded from arrow navigation diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index e0dbd6e94159..eecf5a975684 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -47,15 +47,15 @@ function setClipboardMessage(content: string) { type ShouldShow = ( type: string, - reportAction: ReportAction, + reportAction: OnyxEntry, isArchivedRoom: boolean, - betas: Beta[], + betas: OnyxEntry, menuTarget: HTMLElement, isChronosReport: boolean, reportID: string, isPinnedChat: boolean, isUnreadChat: boolean, - anchor: string, + isOffline: boolean, ) => boolean; type Payload = { @@ -90,7 +90,8 @@ type ContextMenuAction = { const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, - shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), + shouldShow: (type, reportAction) => + type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => { const isMini = !closePopover; @@ -118,7 +119,7 @@ const ContextMenuActions: ContextMenuAction[] = [ onEmojiSelected={toggleEmojiAndCloseMenu} onPressOpenPicker={openContextMenu} onEmojiPickerClosed={closeContextMenu} - reportActionID={reportAction.reportActionID} + reportActionID={reportAction?.reportActionID} reportAction={reportAction} /> ); @@ -130,7 +131,7 @@ const ContextMenuActions: ContextMenuAction[] = [ closeContextMenu={closeContextMenu} onEmojiSelected={toggleEmojiAndCloseMenu} // @ts-expect-error TODO: Remove this once Reactions (https://github.com/Expensify/App/issues/25153) is migrated to TypeScript. - reportActionID={reportAction.reportActionID} + reportActionID={reportAction?.reportActionID} reportAction={reportAction} /> ); @@ -142,11 +143,11 @@ const ContextMenuActions: ContextMenuAction[] = [ icon: Expensicons.Download, successTextTranslateKey: 'common.download', successIcon: Expensicons.Download, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); const messageHtml = reportAction?.message?.at(0)?.html; return ( - isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline + isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction?.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline ); }, onPress: (closePopover, {reportAction}) => { @@ -166,13 +167,13 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.replyInThread', icon: Expensicons.ChatBubble, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => { if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; } - const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; - const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; - const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; + const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); const isModifiedExpenseAction = ReportActionsUtils.isModifiedExpenseAction(reportAction); const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); @@ -199,16 +200,16 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.subscribeToThread', icon: Expensicons.Bell, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => { let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } const subscribed = childReportNotificationPreference !== 'hidden'; - const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); - const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; - const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); }, @@ -235,7 +236,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread', icon: Expensicons.BellSlash, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => { let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); @@ -245,9 +246,9 @@ const ContextMenuActions: ContextMenuAction[] = [ if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; } - const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); - const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; - const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { @@ -310,7 +311,7 @@ const ContextMenuActions: ContextMenuAction[] = [ onPress: (closePopover, {reportAction, selection}) => { const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); - const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction.actionName) : getActionText(reportAction); + const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionText(reportAction); const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); if (!isAttachment) { @@ -374,10 +375,10 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.markAsUnread', icon: Expensicons.Mail, successIcon: Expensicons.Checkmark, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) => + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat), onPress: (closePopover, {reportAction, reportID}) => { - Report.markCommentAsUnread(reportID, reportAction.created); + Report.markCommentAsUnread(reportID, reportAction?.created); if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } @@ -390,7 +391,8 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.markAsRead', icon: Expensicons.Mail, successIcon: Expensicons.Checkmark, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => + type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, onPress: (closePopover, {reportID}) => { Report.readNewestAction(reportID); if (closePopover) { @@ -413,7 +415,7 @@ const ContextMenuActions: ContextMenuAction[] = [ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []); - Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); + Report.openReport(thread.reportID, userLogins, thread, reportAction?.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } @@ -461,7 +463,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'common.pin', icon: Expensicons.Pin, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat, + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat, onPress: (closePopover, {reportID}) => { Report.togglePinnedState(reportID, false); if (closePopover) { @@ -474,7 +476,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'common.unPin', icon: Expensicons.Pin, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat, + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat, onPress: (closePopover, {reportID}) => { Report.togglePinnedState(reportID, true); if (closePopover) { @@ -492,14 +494,14 @@ const ContextMenuActions: ContextMenuAction[] = [ ReportUtils.canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && - reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, + reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { - hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID))); + hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID))); return; } - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID)); + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID)); }, getDescription: () => {}, }, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 7f60b9d9b4d5..5533fe48aefc 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,19 +1,34 @@ -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {Dimensions} from 'react-native'; -import _ from 'underscore'; +import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {Dimensions, EmitterSubscription} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import useLocalize from '@hooks/useLocalize'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; - -function PopoverReportActionContextMenu(_props, ref) { +import {ContextMenuType} from './ReportActionContextMenu'; + +type PopoverReportActionContextMenuRef = { + showContextMenu: () => void; + hideContextMenu: () => void; + showDeleteModal: () => void; + hideDeleteModal: () => void; + isActiveReportAction: () => void; + instanceID: () => void; + runAndResetOnPopoverHide: () => void; + clearActiveReportAction: () => void; + contentRef: () => void; +}; + +function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef) { const {translate} = useLocalize(); const reportIDRef = useRef('0'); - const typeRef = useRef(undefined); - const reportActionRef = useRef({}); + const typeRef = useRef(undefined); + const reportActionRef = useRef>(null); const reportActionIDRef = useRef('0'); const originalReportIDRef = useRef('0'); const selectionRef = useRef(''); @@ -43,7 +58,7 @@ function PopoverReportActionContextMenu(_props, ref) { const contentRef = useRef(null); const anchorRef = useRef(null); - const dimensionsEventListener = useRef(null); + const dimensionsEventListener = useRef(null); const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); @@ -87,8 +102,8 @@ function PopoverReportActionContextMenu(_props, ref) { } popoverAnchorPosition.current = { - horizontal: cursorRelativePosition.horizontal + x, - vertical: cursorRelativePosition.vertical + y, + horizontal: cursorRelativePosition.current.horizontal + x, + vertical: cursorRelativePosition.current.vertical + y, }; }); }, [isPopoverVisible, getContextMenuMeasuredLocation]); @@ -104,36 +119,31 @@ function PopoverReportActionContextMenu(_props, ref) { }; }, [measureContextMenuAnchorPosition]); - /** - * Whether Context Menu is active for the Report Action. - * - * @param {Number|String} actionID - * @return {Boolean} - */ - const isActiveReportAction = (actionID) => Boolean(actionID) && (reportActionIDRef.current === actionID || reportActionRef.current.reportActionID === actionID); + /** Whether Context Menu is active for the Report Action. */ + const isActiveReportAction = (actionID: string): boolean => !!actionID && (reportActionIDRef.current === actionID || reportActionRef.current.reportActionID === actionID); const clearActiveReportAction = () => { reportActionIDRef.current = '0'; - reportActionRef.current = {}; + reportActionRef.current = null; }; /** * Show the ReportActionContextMenu modal popover. * - * @param {string} type - context menu type [EMAIL, LINK, REPORT_ACTION] - * @param {Object} [event] - A press event. - * @param {String} [selection] - Copied content. - * @param {Element} contextMenuAnchor - popoverAnchor - * @param {String} reportID - Active Report Id - * @param {Object} reportActionID - ReportAction for ContextMenu - * @param {String} originalReportID - The currrent Report Id of the reportAction - * @param {String} draftMessage - ReportAction Draftmessage - * @param {Function} [onShow] - Run a callback when Menu is shown - * @param {Function} [onHide] - Run a callback when Menu is hidden - * @param {Boolean} isArchivedRoom - Whether the provided report is an archived room - * @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos - * @param {Boolean} isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action - * @param {Boolean} isUnreadChat - Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action + * @param type - context menu type [EMAIL, LINK, REPORT_ACTION] + * @param [event] - A press event. + * @param [selection] - Copied content. + * @param contextMenuAnchor - popoverAnchor + * @param reportID - Active Report Id + * @param reportActionID - ReportAction for ContextMenu + * @param originalReportID - The currrent Report Id of the reportAction + * @param draftMessage - ReportAction Draftmessage + * @param [onShow] - Run a callback when Menu is shown + * @param [onHide] - Run a callback when Menu is hidden + * @param isArchivedRoom - Whether the provided report is an archived room + * @param isChronosReport - Flag to check if the chat participant is Chronos + * @param isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action + * @param isUnreadChat - Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ const showContextMenu = ( type, @@ -196,8 +206,8 @@ function PopoverReportActionContextMenu(_props, ref) { /** * Run the callback and return a noop function to reset it - * @param {Function} callback - * @returns {Function} + * @param callback + * @returns */ const runAndResetCallback = (callback) => { callback(); @@ -218,10 +228,10 @@ function PopoverReportActionContextMenu(_props, ref) { /** * Hide the ReportActionContextMenu modal popover. - * @param {Function} onHideActionCallback Callback to be called after popover is completely hidden + * @param onHideActionCallback Callback to be called after popover is completely hidden */ const hideContextMenu = (onHideActionCallback) => { - if (_.isFunction(onHideActionCallback)) { + if (onHideActionCallback === 'function') { onPopoverHideActionCallback.current = onHideActionCallback; } @@ -232,10 +242,11 @@ function PopoverReportActionContextMenu(_props, ref) { const confirmDeleteAndHideModal = useCallback(() => { callbackWhenDeleteModalHide.current = () => (onComfirmDeleteModal.current = runAndResetCallback(onComfirmDeleteModal.current)); - if (ReportActionsUtils.isMoneyRequestAction(reportActionRef.current)) { - IOU.deleteMoneyRequest(reportActionRef.current.originalMessage.IOUTransactionID, reportActionRef.current); - } else { - Report.deleteReportComment(reportIDRef.current, reportActionRef.current); + const reportAction = reportActionRef.current; + if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + IOU.deleteMoneyRequest(reportAction?.originalMessage?.IOUTransactionID, reportAction); + } else if (reportAction) { + Report.deleteReportComment(reportIDRef.current, reportAction); } setIsDeleteCommentConfirmModalVisible(false); }, []); @@ -252,11 +263,11 @@ function PopoverReportActionContextMenu(_props, ref) { /** * Opens the Confirm delete action modal - * @param {String} reportID - * @param {Object} reportAction - * @param {Boolean} [shouldSetModalVisibility] - * @param {Function} [onConfirm] - * @param {Function} [onCancel] + * @param reportID + * @param reportAction + * @param [shouldSetModalVisibility] + * @param [onConfirm] + * @param [onCancel] */ const showDeleteModal = (reportID, reportAction, shouldSetModalVisibility = true, onConfirm = () => {}, onCancel = () => {}) => { onCancelDeleteModal.current = onCancel; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index b269bc276b55..7ba4a1e04283 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -173,3 +173,4 @@ function clearActiveReportAction() { } export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, clearActiveReportAction, showDeleteModal, hideDeleteModal}; +export type {ContextMenuType}; From 54e7f76d9f5ede5acb255813a67e1c3f3f857fde Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Sun, 31 Dec 2023 15:49:13 +0100 Subject: [PATCH 016/159] Clean up the states and statuses in the app --- src/CONST.ts | 9 +--- .../ReportActionItem/TaskPreview.js | 4 +- src/libs/ReportUtils.ts | 31 ++++++------- src/libs/actions/IOU.js | 13 +++--- src/libs/actions/Policy.js | 10 ++--- src/libs/actions/Report.ts | 4 +- src/libs/actions/Task.js | 12 ++--- src/libs/actions/Welcome.ts | 2 +- src/pages/home/HeaderView.js | 2 +- src/pages/home/ReportScreen.js | 6 +-- tests/actions/IOUTest.js | 20 ++++----- tests/unit/ReportUtilsTest.js | 38 ++++++++-------- tests/unit/SidebarFilterTest.js | 28 ++++++------ tests/unit/SidebarOrderTest.js | 44 +++++++++---------- tests/unit/SidebarTest.js | 8 ++-- tests/utils/LHNTestUtils.js | 4 +- 16 files changed, 109 insertions(+), 126 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index abba27b0c33b..15cb361eb5db 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -626,18 +626,13 @@ const CONST = { ANNOUNCE: '#announce', ADMINS: '#admins', }, - STATE: { - OPEN: 'OPEN', - SUBMITTED: 'SUBMITTED', - PROCESSING: 'PROCESSING', - }, STATE_NUM: { OPEN: 0, PROCESSING: 1, - SUBMITTED: 2, + APPROVED: 2, BILLING: 3, }, - STATUS: { + STATUS_NUM: { OPEN: 0, SUBMITTED: 1, CLOSED: 2, diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index a7728045f407..578b41c713bc 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -90,8 +90,8 @@ function TaskPreview(props) { // Only the direct parent reportAction will contain details about the taskReport // Other linked reportActions will only contain the taskReportID and we will grab the details from there const isTaskCompleted = !_.isEmpty(props.taskReport) - ? props.taskReport.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && props.taskReport.statusNum === CONST.REPORT.STATUS.APPROVED - : props.action.childStateNum === CONST.REPORT.STATE_NUM.SUBMITTED && props.action.childStatusNum === CONST.REPORT.STATUS.APPROVED; + ? props.taskReport.stateNum === CONST.REPORT.STATE_NUM.APPROVED && props.taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED + : props.action.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && props.action.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; const taskTitle = _.escape(TaskUtils.getTaskTitle(props.taskReportID, props.action.childReportName)); const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(props.taskReport) || props.action.childManagerAccountID; const assigneeLogin = lodashGet(personalDetails, [taskAssigneeAccountID, 'login'], ''); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 24d0050997f7..99a3ec6d796a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -529,14 +529,14 @@ function isCanceledTaskReport(report: OnyxEntry | EmptyObject = {}, pare * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { - return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; + return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; } /** * Checks if a report is a completed task report. */ function isCompletedTaskReport(report: OnyxEntry): boolean { - return isTaskReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; + return isTaskReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED; } /** @@ -550,14 +550,14 @@ function isReportManager(report: OnyxEntry): boolean { * Checks if the supplied report has been approved */ function isReportApproved(report: OnyxEntry | EmptyObject): boolean { - return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; + return report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED; } /** * Checks if the supplied report is an expense report in Open state and status. */ function isDraftExpenseReport(report: OnyxEntry): boolean { - return isExpenseReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; + return isExpenseReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; } /** @@ -588,11 +588,11 @@ function isSettled(reportID: string | undefined): boolean { // In case the payment is scheduled and we are waiting for the payee to set up their wallet, // consider the report as paid as well. - if (report.isWaitingOnBankAccount && report.statusNum === CONST.REPORT.STATUS.APPROVED) { + if (report.isWaitingOnBankAccount && report.statusNum === CONST.REPORT.STATUS_NUM.APPROVED) { return true; } - return report?.statusNum === CONST.REPORT.STATUS.REIMBURSED; + return report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; } /** @@ -767,7 +767,7 @@ function isConciergeChatReport(report: OnyxEntry): boolean { * Returns true if report is still being processed */ function isProcessingReport(report: OnyxEntry): boolean { - return report?.stateNum === CONST.REPORT.STATE_NUM.PROCESSING && report?.statusNum === CONST.REPORT.STATUS.SUBMITTED; + return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED; } /** @@ -873,7 +873,7 @@ function findLastAccessedReport( * Whether the provided report is an archived room */ function isArchivedRoom(report: OnyxEntry | EmptyObject): boolean { - return report?.statusNum === CONST.REPORT.STATUS.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; + return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED; } /** @@ -2469,7 +2469,7 @@ function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: reportAction.reportAction.childType = CONST.REPORT.TYPE.TASK; reportAction.reportAction.childReportName = taskTitle; reportAction.reportAction.childManagerAccountID = taskAssigneeAccountID; - reportAction.reportAction.childStatusNum = CONST.REPORT.STATUS.OPEN; + reportAction.reportAction.childStatusNum = CONST.REPORT.STATUS_NUM.OPEN; reportAction.reportAction.childStateNum = CONST.REPORT.STATE_NUM.OPEN; return reportAction; @@ -2499,9 +2499,8 @@ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number ownerAccountID: payeeAccountID, participantAccountIDs: [payeeAccountID, payerAccountID], reportID: generateReportID(), - state: CONST.REPORT.STATE.SUBMITTED, - stateNum: isSendingMoney ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: isSendingMoney ? CONST.REPORT.STATUS.REIMBURSED : CONST.REPORT.STATE_NUM.PROCESSING, + stateNum: isSendingMoney ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: isSendingMoney ? CONST.REPORT.STATUS_NUM.REIMBURSED : CONST.REPORT.STATE_NUM.SUBMITTED, total, // We don't translate reportName because the server response is always in English @@ -2534,9 +2533,8 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa const isFree = policy?.type === CONST.POLICY.TYPE.FREE; // Define the state and status of the report based on whether the policy is free or paid - const state = isFree ? CONST.REPORT.STATE.SUBMITTED : CONST.REPORT.STATE.OPEN; - const stateNum = isFree ? CONST.REPORT.STATE_NUM.PROCESSING : CONST.REPORT.STATE_NUM.OPEN; - const statusNum = isFree ? CONST.REPORT.STATUS.SUBMITTED : CONST.REPORT.STATUS.OPEN; + const stateNum = isFree ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN; + const statusNum = isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN; return { reportID: generateReportID(), @@ -2548,7 +2546,6 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa // We don't translate reportName because the server response is always in English reportName: `${policyName} owes ${formattedTotal}`, - state, stateNum, statusNum, total: storedTotal, @@ -3248,7 +3245,7 @@ function buildOptimisticTaskReport( parentReportID, policyID, stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, lastVisibleActionCreated: DateUtils.getDBTime(), }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ecae885392b9..2c1d6486ca67 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2827,7 +2827,7 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho lastMessageText: optimisticIOUReportAction.message[0].text, lastMessageHtml: optimisticIOUReportAction.message[0].html, hasOutstandingChildRequest: false, - statusNum: CONST.REPORT.STATUS.REIMBURSED, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }, }, { @@ -2965,8 +2965,8 @@ function approveMoneyRequest(expenseReport) { ...expenseReport, lastMessageText: optimisticApprovedReportAction.message[0].text, lastMessageHtml: optimisticApprovedReportAction.message[0].html, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS.APPROVED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }, }; const optimisticData = [optimisticIOUReportData, optimisticReportActionsData]; @@ -3038,9 +3038,8 @@ function submitReport(expenseReport) { ...expenseReport, lastMessageText: lodashGet(optimisticSubmittedReportAction, 'message.0.text', ''), lastMessageHtml: lodashGet(optimisticSubmittedReportAction, 'message.0.html', ''), - state: CONST.REPORT.STATE.SUBMITTED, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }, }, ...(parentReport.reportID @@ -3084,7 +3083,7 @@ function submitReport(expenseReport) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, value: { - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, stateNum: CONST.REPORT.STATE_NUM.OPEN, }, }, diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f33e6637e2de..2aa583016957 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -172,8 +172,8 @@ function deleteWorkspace(policyID, reports, policyName) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, hasDraft: false, oldPolicyName: allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`].name, }, @@ -352,8 +352,8 @@ function removeMembers(accountIDs, policyID) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, value: { - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, oldPolicyName: policy.name, hasDraft: false, }, @@ -459,7 +459,7 @@ function createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs, hasOutsta key: `${ONYXKEYS.COLLECTION.REPORT}${oldChat.reportID}`, value: { stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, }, }); return; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 06c0316a40b5..701a68ec060a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2079,8 +2079,8 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal } : { reportID: null, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, }, }, diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 0fe6a528cda1..d6f1c920bbd0 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -238,8 +238,8 @@ function completeTask(taskReport) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, value: { - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS.APPROVED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }, }, @@ -267,7 +267,7 @@ function completeTask(taskReport) { key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, value: { stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, }, }, { @@ -306,7 +306,7 @@ function reopenTask(taskReport) { key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, value: { stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, lastVisibleActionCreated: reopenedTaskReportAction.created, lastMessageText: message, lastActorAccountID: reopenedTaskReportAction.actorAccountID, @@ -336,8 +336,8 @@ function reopenTask(taskReport) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, value: { - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS.APPROVED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }, }, { diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index 02109804efb9..fdf68b72fc3f 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -131,7 +131,7 @@ function show({routes, showCreateMenu = () => {}, showPopoverMenu = () => false} const workspaceChatReport = Object.values(allReports ?? {}).find((report) => { if (report) { - return ReportUtils.isPolicyExpenseChat(report) && report.ownerAccountID === currentUserAccountID && report.statusNum !== CONST.REPORT.STATUS.CLOSED; + return ReportUtils.isPolicyExpenseChat(report) && report.ownerAccountID === currentUserAccountID && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED; } return false; }); diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index d3ec573b5886..4d77fb90622f 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -132,7 +132,7 @@ function HeaderView(props) { } // Task is not closed - if (props.report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum !== CONST.REPORT.STATUS.CLOSED && canModifyTask) { + if (props.report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && props.report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { threeDotMenuItems.push({ icon: Expensicons.Trashcan, text: translate('common.cancel'), diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 8e177e0c2e64..38ee1e81fc77 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -186,7 +186,7 @@ function ReportScreen({ // There are no reportActions at all to display and we are still in the process of loading the next set of actions. const isLoadingInitialReportActions = _.isEmpty(filteredReportActions) && reportMetadata.isLoadingInitialReportActions; - const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; + const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS_NUM.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); @@ -358,8 +358,8 @@ function ReportScreen({ (prevOnyxReportID && prevOnyxReportID === routeReportID && !onyxReportID && - prevReport.statusNum === CONST.REPORT.STATUS.OPEN && - (report.statusNum === CONST.REPORT.STATUS.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || + prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && + (report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport)) && _.isEmpty(report)) ) { Navigation.dismissModal(); diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 4d9ce42a08ce..492726b1865a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -1237,9 +1237,8 @@ describe('actions/IOU', () => { expect(chatReport.pendingFields).toBeFalsy(); expect(iouReport.pendingFields).toBeFalsy(); - // expect(iouReport.status).toBe(CONST.REPORT.STATUS.SUBMITTED); - // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); - // expect(iouReport.state).toBe(CONST.REPORT.STATE.SUBMITTED); + // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); + // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); resolve(); }, @@ -1306,9 +1305,8 @@ describe('actions/IOU', () => { expect(chatReport.iouReportID).toBeFalsy(); - // expect(iouReport.status).toBe(CONST.REPORT.STATUS.REIMBURSED); - // expect(iouReport.state).toBe(CONST.REPORT.STATE.MANUALREIMBURSED); - // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); + // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); + // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); resolve(); }, @@ -1356,9 +1354,8 @@ describe('actions/IOU', () => { expect(chatReport.iouReportID).toBeFalsy(); - // expect(iouReport.status).toBe(CONST.REPORT.STATUS.REIMBURSED); - // expect(iouReport.state).toBe(CONST.REPORT.STATE.MANUALREIMBURSED); - // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); + // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); + // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); resolve(); }, @@ -1770,9 +1767,8 @@ describe('actions/IOU', () => { expect.objectContaining({ lastMessageHtml: `paid $${amount / 100}.00 with Expensify`, lastMessageText: `paid $${amount / 100}.00 with Expensify`, - state: CONST.REPORT.STATE.SUBMITTED, - statusNum: CONST.REPORT.STATUS.REIMBURSED, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, }), ); expect(updatedChatReport).toEqual( diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index d700aa4724f1..c9e8053e3146 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -149,8 +149,8 @@ describe('ReportUtils', () => { test('Archived', () => { const archivedAdminsRoom = { ...baseAdminsRoom, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; expect(ReportUtils.getReportName(archivedAdminsRoom)).toBe('#admins (archived)'); @@ -172,8 +172,8 @@ describe('ReportUtils', () => { test('Archived', () => { const archivedPolicyRoom = { ...baseUserCreatedRoom, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; expect(ReportUtils.getReportName(archivedPolicyRoom)).toBe('#VikingsChat (archived)'); @@ -213,8 +213,8 @@ describe('ReportUtils', () => { ownerAccountID: 1, policyID: policy.policyID, oldPolicyName: policy.name, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; test('as member', () => { @@ -307,7 +307,7 @@ describe('ReportUtils', () => { managerID: currentUserAccountID, isUnreadWithMention: false, stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); @@ -368,7 +368,7 @@ describe('ReportUtils', () => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.IOU, - statusNum: CONST.REPORT.STATUS.REIMBURSED, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); @@ -378,8 +378,8 @@ describe('ReportUtils', () => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS.APPROVED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); @@ -389,7 +389,7 @@ describe('ReportUtils', () => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, - statusNum: CONST.REPORT.STATUS.REIMBURSED, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]); expect(moneyRequestOptions.length).toBe(0); @@ -419,8 +419,8 @@ describe('ReportUtils', () => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, parentReportID: '101', }; const paidPolicy = { @@ -508,7 +508,7 @@ describe('ReportUtils', () => { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, parentReportID: '103', }; const paidPolicy = { @@ -523,9 +523,8 @@ describe('ReportUtils', () => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.IOU, - state: CONST.REPORT.STATE.SUBMITTED, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); @@ -536,9 +535,8 @@ describe('ReportUtils', () => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.IOU, - state: CONST.REPORT.STATE.SUBMITTED, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(1); diff --git a/tests/unit/SidebarFilterTest.js b/tests/unit/SidebarFilterTest.js index dd2985ea34a8..35d91e291666 100644 --- a/tests/unit/SidebarFilterTest.js +++ b/tests/unit/SidebarFilterTest.js @@ -471,20 +471,20 @@ describe('Sidebar', () => { // Given an archived chat report, an archived default policy room, and an archived user created policy room const archivedReport = { ...LHNTestUtils.getFakeReport([1, 2]), - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const archivedPolicyRoomReport = { ...LHNTestUtils.getFakeReport([1, 2]), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const archivedUserCreatedPolicyRoomReport = { ...LHNTestUtils.getFakeReport([1, 2]), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; LHNTestUtils.getDefaultRenderedSidebarLinks(); @@ -695,8 +695,8 @@ describe('Sidebar', () => { const report = { ...LHNTestUtils.getFakeReport(), lastVisibleActionCreated: '2022-11-22 03:48:27.267', - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; // Given the user is in all betas @@ -746,8 +746,8 @@ describe('Sidebar', () => { // Given an archived report that has all comments read const report = { ...LHNTestUtils.getFakeReport(), - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; // Given the user is in all betas @@ -795,8 +795,8 @@ describe('Sidebar', () => { const report = { ...LHNTestUtils.getFakeReport(), isPinned: false, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; // Given the user is in all betas @@ -840,8 +840,8 @@ describe('Sidebar', () => { // Given an archived report that is not the active report const report = { ...LHNTestUtils.getFakeReport(), - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; // Given the user is in all betas diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 44d6dd57de91..a6b0f4dba60d 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -255,7 +255,7 @@ describe('Sidebar', () => { reportName: taskReportName, managerID: 2, stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, }; // Each report has at least one ADDCOMMENT action so should be rendered in the LNH @@ -313,8 +313,8 @@ describe('Sidebar', () => { total: 10000, currency: 'USD', chatReportID: report3.reportID, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; report3.iouReportID = iouReport.reportID; @@ -374,9 +374,8 @@ describe('Sidebar', () => { policyName: 'Workspace', total: -10000, currency: 'USD', - state: CONST.REPORT.STATE.SUBMITTED, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, chatReportID: report3.reportID, parentReportID: report3.reportID, }; @@ -575,9 +574,8 @@ describe('Sidebar', () => { total: 10000, currency: 'USD', chatReportID: report3.reportID, - state: CONST.REPORT.STATE.SUBMITTED, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; report3.iouReportID = iouReport.reportID; const currentReportId = report2.reportID; @@ -740,8 +738,8 @@ describe('Sidebar', () => { const report1 = { ...LHNTestUtils.getFakeReport([1, 2]), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const report2 = LHNTestUtils.getFakeReport([3, 4]); const report3 = LHNTestUtils.getFakeReport([5, 6]); @@ -837,8 +835,8 @@ describe('Sidebar', () => { const report1 = { ...LHNTestUtils.getFakeReport([1, 2], 3, true), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const report2 = LHNTestUtils.getFakeReport([3, 4], 2, true); const report3 = LHNTestUtils.getFakeReport([5, 6], 1, true); @@ -914,8 +912,8 @@ describe('Sidebar', () => { total: 10000, currency: 'USD', chatReportID: report3.reportID, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; const iouReport2 = { ...LHNTestUtils.getFakeReport([9, 10]), @@ -926,8 +924,8 @@ describe('Sidebar', () => { total: 10000, currency: 'USD', chatReportID: report3.reportID, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; const iouReport3 = { ...LHNTestUtils.getFakeReport([11, 12]), @@ -938,8 +936,8 @@ describe('Sidebar', () => { total: 100000, currency: 'USD', chatReportID: report3.reportID, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; const iouReport4 = { ...LHNTestUtils.getFakeReport([11, 12]), @@ -950,8 +948,8 @@ describe('Sidebar', () => { total: 10000, currency: 'USD', chatReportID: report3.reportID, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; const iouReport5 = { ...LHNTestUtils.getFakeReport([11, 12]), @@ -962,8 +960,8 @@ describe('Sidebar', () => { total: 10000, currency: 'USD', chatReportID: report3.reportID, - stateNum: CONST.REPORT.STATE_NUM.PROCESSING, - statusNum: CONST.REPORT.STATUS.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }; report1.iouReportID = iouReport1.reportID; diff --git a/tests/unit/SidebarTest.js b/tests/unit/SidebarTest.js index 106b2c3b69a9..09d3905fe86a 100644 --- a/tests/unit/SidebarTest.js +++ b/tests/unit/SidebarTest.js @@ -51,8 +51,8 @@ describe('Sidebar', () => { const report = { ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true), chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; // Given the user is in all betas @@ -86,8 +86,8 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true), policyName: 'Vikings Policy', chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, - statusNum: CONST.REPORT.STATUS.CLOSED, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, }; const action = { ...LHNTestUtils.getFakeReportAction('email1@test.com', 3, true), diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index a80feae730c6..0507a671358a 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -209,8 +209,8 @@ function getAdvancedFakeReport(isArchived, isUserCreatedPolicyRoom, hasAddWorksp ...getFakeReport([1, 2], 0, isUnread), type: CONST.REPORT.TYPE.CHAT, chatType: isUserCreatedPolicyRoom ? CONST.REPORT.CHAT_TYPE.POLICY_ROOM : CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, - statusNum: isArchived ? CONST.REPORT.STATUS.CLOSED : 0, - stateNum: isArchived ? CONST.REPORT.STATE_NUM.SUBMITTED : 0, + statusNum: isArchived ? CONST.REPORT.STATUS_NUM.CLOSED : 0, + stateNum: isArchived ? CONST.REPORT.STATE_NUM.APPROVED : 0, errorFields: hasAddWorkspaceError ? {addWorkspaceRoom: 'blah'} : null, isPinned, hasDraft, From b93661414bdaa7f0bb24da494a75b0c4777c1b56 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Sun, 31 Dec 2023 15:57:34 +0100 Subject: [PATCH 017/159] remove the state from the Report type --- src/types/onyx/Report.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index b274025908f5..9db1de108885 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -83,9 +83,6 @@ type Report = { /** ID of the chat report */ chatReportID?: string; - /** The state of the report */ - state?: ValueOf; - /** The state that the report is currently in */ stateNum?: ValueOf; From 3877497f806050a569941170a219cc299ac79eb7 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Sun, 31 Dec 2023 16:09:25 +0100 Subject: [PATCH 018/159] Prettier --- src/libs/ReportUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 99a3ec6d796a..40101feb6d40 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -529,7 +529,9 @@ function isCanceledTaskReport(report: OnyxEntry | EmptyObject = {}, pare * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { - return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; + return ( + isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN + ); } /** From a5d07180966c77bdd0d2e5d3e358d8a6911161c0 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Sun, 31 Dec 2023 16:20:52 +0100 Subject: [PATCH 019/159] Fix the statenum const --- src/CONST.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 15cb361eb5db..62e79ec62400 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -628,7 +628,7 @@ const CONST = { }, STATE_NUM: { OPEN: 0, - PROCESSING: 1, + SUBMITTED: 1, APPROVED: 2, BILLING: 3, }, From 02d8deb0c84212f158c95194067b7bd23cc5f52b Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Sun, 31 Dec 2023 16:29:07 +0100 Subject: [PATCH 020/159] Fix type issues --- src/libs/E2E/apiMocks/openApp.ts | 4 ++-- src/libs/ReportUtils.ts | 4 +--- src/types/onyx/Report.ts | 2 +- src/types/onyx/ReportAction.ts | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libs/E2E/apiMocks/openApp.ts b/src/libs/E2E/apiMocks/openApp.ts index 42d13716407d..4c12a95a1db4 100644 --- a/src/libs/E2E/apiMocks/openApp.ts +++ b/src/libs/E2E/apiMocks/openApp.ts @@ -2043,10 +2043,10 @@ const openApp = (): Response => ({ managerID: 16, currency: 'USD', chatReportID: '98817646', - state: 'SUBMITTED', cachedTotal: '($1,473.11)', total: 147311, stateNum: 1, + statusNum: 1, }, report_4249286573496381: { reportID: '4249286573496381', @@ -2054,10 +2054,10 @@ const openApp = (): Response => ({ managerID: 21, currency: 'USD', chatReportID: '4867098979334014', - state: 'SUBMITTED', cachedTotal: '($212.78)', total: 21278, stateNum: 1, + statusNum: 1, }, }, }, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 40101feb6d40..5c5fe45fc811 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -109,7 +109,6 @@ type OptimisticExpenseReport = Pick< | 'ownerAccountID' | 'currency' | 'reportName' - | 'state' | 'stateNum' | 'statusNum' | 'total' @@ -300,13 +299,12 @@ type OptimisticIOUReport = Pick< | 'ownerAccountID' | 'participantAccountIDs' | 'reportID' - | 'state' | 'stateNum' + | 'statusNum' | 'total' | 'reportName' | 'notificationPreference' | 'parentReportID' - | 'statusNum' | 'lastVisibleActionCreated' >; type DisplayNameWithTooltips = Array>; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 9db1de108885..607c303f1abb 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -87,7 +87,7 @@ type Report = { stateNum?: ValueOf; /** The status of the current report */ - statusNum?: ValueOf; + statusNum?: ValueOf; /** Which user role is capable of posting messages on the report */ writeCapability?: WriteCapability; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index a881b63fbb95..df4248b90d09 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -148,7 +148,7 @@ type ReportActionBase = { childManagerAccountID?: number; /** The status of the child report */ - childStatusNum?: ValueOf; + childStatusNum?: ValueOf; /** Report action child status name */ childStateNum?: ValueOf; From 0b7592d8efb3242816eb5aebba4120dc22e9a131 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 2 Jan 2024 11:55:17 +0500 Subject: [PATCH 021/159] fix: lint issues --- src/CONST.ts | 2 + .../ReportActionItem/MoneyReportView.tsx | 61 +++++++++++-------- src/libs/ReportUtils.ts | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index abba27b0c33b..e0479869987b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1427,6 +1427,8 @@ const CONST = { INVISIBLE_CHARACTERS_GROUPS: /[\p{C}\p{Z}]/gu, OTHER_INVISIBLE_CHARACTERS: /[\u3164]/g, + + REPORT_FIELD_TITLE: /{report:([a-zA-Z]+)}/g, }, PRONOUNS: { diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 2ffbda4d347b..9ad9e73244f3 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -1,12 +1,13 @@ -import React, { useMemo } from 'react'; +import React, {useMemo} from 'react'; import {StyleProp, TextStyle, View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import SpacerView from '@components/SpacerView'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import SpacerView from '@components/SpacerView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -16,7 +17,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; import type {PolicyReportField, Report} from '@src/types/onyx'; -import usePermissions from '@hooks/usePermissions'; type MoneyReportViewProps = { /** The report currently being looked at */ @@ -52,34 +52,41 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: StyleUtils.getColorStyle(theme.textSupporting), ]; - const sortedPolicyReportFields = useMemo(() => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight), [policyReportFields]); + const sortedPolicyReportFields = useMemo( + () => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight), + [policyReportFields], + ); return ( - {canUseReportFields && sortedPolicyReportFields.map((reportField) => { - const title = ReportUtils.getReportFieldTitle(report, reportField); - return ( - - {}} - shouldShowRightIcon - disabled={false} - wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} - shouldGreyOutWhenDisabled={false} - numberOfLinesTitle={0} - interactive - shouldStackHorizontally={false} - onSecondaryInteraction={() => {}} - hoverAndPressStyle={false} - titleWithTooltips={[]} - /> - - ); - })} + {canUseReportFields && + sortedPolicyReportFields.map((reportField) => { + const title = ReportUtils.getReportFieldTitle(report, reportField); + return ( + + {}} + shouldShowRightIcon + disabled={false} + wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} + shouldGreyOutWhenDisabled={false} + numberOfLinesTitle={0} + interactive + shouldStackHorizontally={false} + onSecondaryInteraction={() => {}} + hoverAndPressStyle={false} + titleWithTooltips={[]} + /> + + ); + })} , reportField: PolicyRepor return value; } - return value.replaceAll(/{report:([a-zA-Z]+)}/g, (match, property) => { + return value.replaceAll(CONST.REGEX.REPORT_FIELD_TITLE, (match, property) => { if (report && property in report) { return report[property as keyof Report]?.toString() ?? match; } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 4e806faa1b47..a16ba6132ebc 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -761,7 +761,7 @@ export default compose( initialValue: {}, }, policyReportFields: { - key: ({report}) => report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined, + key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), initialValue: [], }, emojiReactions: { From e4a4bdf9ae5ec7f026c643dc45eccc2caae5d9e7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jan 2024 16:30:30 +0100 Subject: [PATCH 022/159] Fix MenuItem types --- src/components/MenuItem.tsx | 245 +++++++++--------- .../BaseVideoChatButtonAndMenu.tsx | 5 +- 2 files changed, 121 insertions(+), 129 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index db150d55f0d2..2b192aba2f2e 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -33,20 +33,6 @@ import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -type ResponsiveProps = { - /** Function to fire when component is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent) => void; - - interactive?: true; -}; - -type UnresponsiveProps = { - onPress?: undefined; - - /** Whether the menu item should be interactive at all */ - interactive: false; -}; - type IconProps = { /** Flag to choose between avatar image or an icon */ iconType: typeof CONST.ICON_TYPE_ICON; @@ -56,7 +42,7 @@ type IconProps = { }; type AvatarProps = { - iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + iconType?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; icon: AvatarSource; }; @@ -67,170 +53,175 @@ type NoIcon = { icon?: undefined; }; -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & - (IconProps | AvatarProps | NoIcon) & { - /** Text to be shown as badge near the right end. */ - badgeText?: string; +type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { + /** Function to fire when component is pressed */ + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; - /** Used to apply offline styles to child text components */ - style?: ViewStyle; + /** Whether the menu item should be interactive at all */ + interactive?: boolean; - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; + /** Text to be shown as badge near the right end. */ + badgeText?: string; - /** Any additional styles to apply on the outer element */ - containerStyle?: StyleProp; + /** Used to apply offline styles to child text components */ + style?: StyleProp; - /** Used to apply styles specifically to the title */ - titleStyle?: ViewStyle; + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: StyleProp>; + /** Any additional styles to apply on the outer element */ + containerStyle?: StyleProp; - /** Additional styles to style the description text below the title */ - descriptionTextStyle?: StyleProp; + /** Used to apply styles specifically to the title */ + titleStyle?: ViewStyle; - /** The fill color to pass into the icon. */ - iconFill?: string; + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle?: StyleProp>; - /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon?: IconAsset; + /** Additional styles to style the description text below the title */ + descriptionTextStyle?: StyleProp; - /** The fill color to pass into the secondary icon. */ - secondaryIconFill?: string; + /** The fill color to pass into the icon. */ + iconFill?: string; - /** Icon Width */ - iconWidth?: number; + /** Secondary icon to display on the left side of component, right of the icon */ + secondaryIcon?: IconAsset; - /** Icon Height */ - iconHeight?: number; + /** The fill color to pass into the secondary icon. */ + secondaryIconFill?: string; - /** Any additional styles to pass to the icon container. */ - iconStyles?: StyleProp; + /** Icon Width */ + iconWidth?: number; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: IconAsset; + /** Icon Height */ + iconHeight?: number; - /** An icon to display under the main item */ - furtherDetailsIcon?: IconAsset; + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon?: boolean; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: IconAsset; - /** Icon to display at right side of title */ - titleIcon?: IconAsset; + /** An icon to display under the main item */ + furtherDetailsIcon?: IconAsset; - /** Boolean whether to display the right icon */ - shouldShowRightIcon?: boolean; + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon?: boolean; - /** Overrides the icon for shouldShowRightIcon */ - iconRight?: IconAsset; + /** Icon to display at right side of title */ + titleIcon?: IconAsset; - /** Should render component on the right */ - shouldShowRightComponent?: boolean; + /** Boolean whether to display the right icon */ + shouldShowRightIcon?: boolean; - /** Component to be displayed on the right */ - rightComponent?: ReactNode; + /** Overrides the icon for shouldShowRightIcon */ + iconRight?: IconAsset; - /** A description text to show under the title */ - description?: string; + /** Should render component on the right */ + shouldShowRightComponent?: boolean; - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop?: boolean; + /** Component to be displayed on the right */ + rightComponent?: ReactNode; - /** Error to display below the title */ - error?: string; + /** A description text to show under the title */ + description?: string; - /** Error to display at the bottom of the component */ - errorText?: string; + /** Should the description be shown above the title (instead of the other way around) */ + shouldShowDescriptionOnTop?: boolean; - /** A boolean flag that gives the icon a green fill if true */ - success?: boolean; + /** Error to display below the title */ + error?: string; - /** Whether item is focused or active */ - focused?: boolean; + /** Error to display at the bottom of the component */ + errorText?: string; - /** Should we disable this menu item? */ - disabled?: boolean; + /** A boolean flag that gives the icon a green fill if true */ + success?: boolean; - /** Text that appears above the title */ - label?: string; + /** Whether item is focused or active */ + focused?: boolean; - /** Label to be displayed on the right */ - rightLabel?: string; + /** Should we disable this menu item? */ + disabled?: boolean; - /** Text to display for the item */ - title?: string; + /** Text that appears above the title */ + label?: string; - /** A right-aligned subtitle for this menu option */ - subtitle?: string | number; + /** Label to be displayed on the right */ + rightLabel?: string; - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle?: boolean; + /** Text to display for the item */ + title?: string; - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState?: boolean; + /** A right-aligned subtitle for this menu option */ + subtitle?: string | number; - /** Whether this item is selected */ - isSelected?: boolean; + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle?: boolean; - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState?: boolean; - /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; + /** Whether this item is selected */ + isSelected?: boolean; - /** Avatars to show on the right of the menu item */ - floatRightAvatars?: IconType[]; + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally?: boolean; - /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize?: ValueOf; + /** Prop to represent the size of the avatar images to be shown */ + avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; - /** Affects avatar size */ - viewMode?: ValueOf; + /** Avatars to show on the right of the menu item */ + floatRightAvatars?: IconType[]; - /** Used to truncate the text with an ellipsis after computing the text layout */ - numberOfLinesTitle?: number; + /** Prop to represent the size of the float right avatar images to be shown */ + floatRightAvatarSize?: ValueOf; - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu?: boolean; + /** Affects avatar size */ + viewMode?: ValueOf; - /** The type of brick road indicator to show. */ - brickRoadIndicator?: ValueOf; + /** Used to truncate the text with an ellipsis after computing the text layout */ + numberOfLinesTitle?: number; - /** Should render the content in HTML format */ - shouldRenderAsHTML?: boolean; + /** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu?: boolean; - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled?: boolean; + /** The type of brick road indicator to show. */ + brickRoadIndicator?: ValueOf; - /** The action accept for anonymous user or not */ - isAnonymousAction?: boolean; + /** Should render the content in HTML format */ + shouldRenderAsHTML?: boolean; - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection?: boolean; + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled?: boolean; - /** Whether should render title as HTML or as Text */ - shouldParseTitle?: false; + /** The action accept for anonymous user or not */ + isAnonymousAction?: boolean; - /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress?: boolean; + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection?: boolean; - /** Text to display under the main item */ - furtherDetails?: string; + /** Whether should render title as HTML or as Text */ + shouldParseTitle?: false; - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; + /** Should check anonymous user in onPress function */ + shouldCheckActionAllowedOnPress?: boolean; - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + /** Text to display under the main item */ + furtherDetails?: string; - /** Icon should be displayed in its own color */ - displayInDefaultIconColor?: boolean; + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction?: () => void; - /** Determines how the icon should be resized to fit its container */ - contentFit?: ImageContentFit; - }; + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips?: DisplayNameWithTooltip[]; + + /** Icon should be displayed in its own color */ + displayInDefaultIconColor?: boolean; + + /** Determines how the icon should be resized to fit its container */ + contentFit?: ImageContentFit; +}; function MenuItem( { @@ -298,7 +289,7 @@ function MenuItem( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const combinedStyle = StyleUtils.combineStyles(style ?? {}, styles.popoverMenuItem); + const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = useState(''); const titleRef = useRef(''); diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx index 906f9530af9f..c180fa670efc 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx @@ -29,8 +29,8 @@ function BaseVideoChatButtonAndMenu(props: BaseVideoChatButtonAndMenuProps) { const {isSmallScreenWidth} = useWindowDimensions(); const [isVideoChatMenuActive, setIsVideoChatMenuActive] = useState(false); const [videoChatIconPosition, setVideoChatIconPosition] = useState({x: 0, y: 0}); - const videoChatIconWrapperRef = useRef(null); - const videoChatButtonRef = useRef(null); + const videoChatIconWrapperRef = useRef(null); + const videoChatButtonRef = useRef(null); const menuItemData = [ { @@ -124,6 +124,7 @@ function BaseVideoChatButtonAndMenu(props: BaseVideoChatButtonAndMenuProps) { wrapperStyle={styles.mr3} key={text} icon={icon} + iconType={CONST.ICON_TYPE_ICON} title={text} onPress={onPress} /> From d0c910e7ddce28a923aa400694f800013e1a5cbd Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 2 Jan 2024 16:36:04 +0100 Subject: [PATCH 023/159] Use report.lastMessageText as the all-case fallback for getLastMessageTextForReport --- src/libs/OptionsListUtils.js | 10 +++++----- src/libs/PersonalDetailsUtils.ts | 32 ++++++++++++++++++++++++++++++++ src/libs/SidebarUtils.ts | 2 +- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 0d5162399fcb..1e8429810671 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -379,9 +379,10 @@ function getAllReportErrors(report, reportActions) { /** * Get the last message text from the report directly or from other sources for special cases. * @param {Object} report + * @param {Object[]} personalDetails - list of personal details of the report participants * @returns {String} */ -function getLastMessageTextForReport(report) { +function getLastMessageTextForReport(report, personalDetails) { const lastReportAction = _.find(allSortedReportActions[report.reportID], (reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)); let lastMessageTextFromReport = ''; const lastActionName = lodashGet(lastReportAction, 'actionName', ''); @@ -418,10 +419,9 @@ function getLastMessageTextForReport(report) { lastMessageTextFromReport = lodashGet(lastReportAction, 'message[0].text', ''); } else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) { lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction); - } else { - lastMessageTextFromReport = report ? report.lastMessageText || '' : ''; } - return lastMessageTextFromReport; + + return lastMessageTextFromReport || PersonalDetailsUtils.replaceLoginsWithDisplayNames(lodashGet(report, 'lastMessageText', ''), personalDetails); } /** @@ -509,7 +509,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; subtitle = ReportUtils.getChatRoomSubtitle(report); - const lastMessageTextFromReport = getLastMessageTextForReport(report); + const lastMessageTextFromReport = getLastMessageTextForReport(report, personalDetailList); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; const lastActorDisplayName = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? lastActorDetails.firstName || lastActorDetails.displayName : ''; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index a5a033797c7b..00007b5e04d2 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -6,13 +6,28 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; +let currentUserAccountID: number | undefined; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + // When signed out, val is undefined + if (!value) { + return; + } + + currentUserAccountID = value.accountID; + }, +}); + let personalDetails: Array = []; let allPersonalDetails: OnyxEntry = {}; +let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => { personalDetails = Object.values(val ?? {}); allPersonalDetails = val; + currentUserPersonalDetails = val?.[currentUserAccountID ?? -1] ?? null; }, }); @@ -27,6 +42,22 @@ function getDisplayNameOrDefault(displayName?: string, defaultValue = ''): strin return displayName || defaultValue || Localize.translateLocal('common.hidden'); } +function replaceLoginsWithDisplayNames(text: string, details: PersonalDetails[], includeCurrentAccount = true): string { + const result = details.reduce((replacedText, detail) => { + if (!detail.login) { + return replacedText; + } + + return replacedText.replaceAll(detail.login, getDisplayNameOrDefault(detail?.displayName)); + }, text); + + if (!includeCurrentAccount || !currentUserPersonalDetails) { + return result; + } + + return result.replaceAll(currentUserPersonalDetails.login ?? '', getDisplayNameOrDefault(currentUserPersonalDetails?.displayName)); +} + /** * Given a list of account IDs (as number) it will return an array of personal details objects. * @param accountIDs - Array of accountIDs @@ -212,4 +243,5 @@ export { getFormattedStreet, getStreetLines, getEffectiveDisplayName, + replaceLoginsWithDisplayNames, }; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index c4fad1a86906..cf90f87ab5e3 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -319,7 +319,7 @@ function getOptionData( // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); - const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report); + const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report, displayNamesWithTooltips); // If the last actor's details are not currently saved in Onyx Collection, // then try to get that from the last report action if that action is valid From 0ab996ceb60081b96f464c3597c10cfe79693d72 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jan 2024 17:22:05 +0100 Subject: [PATCH 024/159] Fix ref types --- src/components/Modal/types.ts | 4 +- src/components/Popover/types.ts | 42 ++++++++++--------- src/components/PopoverProvider/types.ts | 8 ++-- .../PopoverWithoutOverlay/index.tsx | 3 +- src/components/PopoverWithoutOverlay/types.ts | 4 +- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 461a5935eda9..af6d90268521 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -1,4 +1,4 @@ -import {ViewStyle} from 'react-native'; +import {View, ViewStyle} from 'react-native'; import {ModalProps} from 'react-native-modal'; import {ValueOf} from 'type-fest'; import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; @@ -23,7 +23,7 @@ type BaseModalProps = WindowDimensionsProps & shouldSetModalVisibility?: boolean; /** Callback method fired when the user requests to close the modal */ - onClose: (ref?: React.RefObject) => void; + onClose: (ref?: React.RefObject) => void; /** State that determines whether to display the modal or not */ isVisible: boolean; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 103ab0404081..f58f282c599d 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -1,38 +1,42 @@ -import {ReactNode, RefObject} from 'react'; +import {RefObject} from 'react'; import {View} from 'react-native'; import BaseModalProps, {PopoverAnchorPosition} from '@components/Modal/types'; import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; + +type AnchorAlignment = {horizontal: string; vertical: string}; type PopoverDimensions = { width: number; height: number; }; -type PopoverProps = BaseModalProps & { - /** The anchor position of the popover */ - anchorPosition?: PopoverAnchorPosition; +type PopoverProps = BaseModalProps & + ChildrenProps & { + /** The anchor position of the popover */ + anchorPosition?: PopoverAnchorPosition; - /** The anchor ref of the popover */ - anchorRef: RefObject; + /** The anchor alignment of the popover */ + anchorAlignment?: AnchorAlignment; - /** Whether disable the animations */ - disableAnimation?: boolean; + /** The anchor ref of the popover */ + anchorRef: RefObject; - /** Whether we don't want to show overlay */ - withoutOverlay: boolean; + /** Whether disable the animations */ + disableAnimation?: boolean; - /** The dimensions of the popover */ - popoverDimensions?: PopoverDimensions; + /** Whether we don't want to show overlay */ + withoutOverlay: boolean; - /** The ref of the popover */ - withoutOverlayRef?: RefObject; + /** The dimensions of the popover */ + popoverDimensions?: PopoverDimensions; - /** Whether we want to show the popover on the right side of the screen */ - fromSidebarMediumScreen?: boolean; + /** The ref of the popover */ + withoutOverlayRef?: RefObject; - /** The popover children */ - children: ReactNode; -}; + /** Whether we want to show the popover on the right side of the screen */ + fromSidebarMediumScreen?: boolean; + }; type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps; diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts index dc0208e10dd7..a638ed982196 100644 --- a/src/components/PopoverProvider/types.ts +++ b/src/components/PopoverProvider/types.ts @@ -8,14 +8,14 @@ type PopoverContextProps = { type PopoverContextValue = { onOpen?: (popoverParams: AnchorRef) => void; popover?: AnchorRef | Record | null; - close: (anchorRef?: RefObject) => void; + close: (anchorRef?: RefObject) => void; isOpen: boolean; }; type AnchorRef = { - ref: RefObject; - close: (anchorRef?: RefObject) => void; - anchorRef: RefObject; + ref: RefObject; + close: (anchorRef?: RefObject) => void; + anchorRef: RefObject; onOpenCallback?: () => void; onCloseCallback?: () => void; }; diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index f83949bcbe9d..0d4ef7d2e912 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -7,6 +7,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Modal from '@userActions/Modal'; +import viewRef from '@src/types/utils/viewRef'; import PopoverWithoutOverlayProps from './types'; function PopoverWithoutOverlay( @@ -118,7 +119,7 @@ function PopoverWithoutOverlay( return ( ; + anchorRef: React.RefObject; /** A react-native-animatable animation timing for the modal display animation */ animationInTiming?: number; @@ -22,7 +22,7 @@ type PopoverWithoutOverlayProps = ChildrenProps & disableAnimation?: boolean; /** The ref of the popover */ - withoutOverlayRef: React.RefObject; + withoutOverlayRef: React.RefObject; }; export default PopoverWithoutOverlayProps; From a2508343aa10ec039299fbccf7aeab2c62014122 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 2 Jan 2024 17:41:12 +0100 Subject: [PATCH 025/159] ref: migrate ReportAttachments page to Typescript --- ...rtAttachments.js => ReportAttachments.tsx} | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) rename src/pages/home/report/{ReportAttachments.js => ReportAttachments.tsx} (55%) diff --git a/src/pages/home/report/ReportAttachments.js b/src/pages/home/report/ReportAttachments.tsx similarity index 55% rename from src/pages/home/report/ReportAttachments.js rename to src/pages/home/report/ReportAttachments.tsx index 8ecbb036a756..1558cc8a13c4 100644 --- a/src/pages/home/report/ReportAttachments.js +++ b/src/pages/home/report/ReportAttachments.tsx @@ -1,43 +1,45 @@ -import PropTypes from 'prop-types'; +import {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback} from 'react'; -import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import Navigation from '@libs/Navigation/Navigation'; +import {AuthScreensParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - /** Route specific parameters used on this screen */ - params: PropTypes.shape({ - /** The report ID which the attachment is associated with */ - reportID: PropTypes.string.isRequired, - /** The uri encoded source of the attachment */ - source: PropTypes.string.isRequired, - }).isRequired, - }).isRequired, +type Attachment = { + file: { + name: string; + }; + hasBeenFlagged: boolean; + isAuthTokenRequired: boolean; + isReceipt: boolean; + reportActionID: string; + source: string; }; -function ReportAttachments(props) { - const reportID = _.get(props, ['route', 'params', 'reportID']); +type ReportAttachmentsProps = StackScreenProps; + +function ReportAttachments({route}: ReportAttachmentsProps) { + const reportID = route.params?.reportID; const report = ReportUtils.getReport(reportID); // In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource - const decodedSource = decodeURI(_.get(props, ['route', 'params', 'source'])); + const decodedSource = decodeURI(route.params.source); const source = Number(decodedSource) || decodedSource; const onCarouselAttachmentChange = useCallback( - (attachment) => { - const route = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, attachment.source); - Navigation.navigate(route); + (attachment: Attachment) => { + const routeToNavigate = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, attachment.source); + Navigation.navigate(routeToNavigate); }, [reportID], ); return ( Date: Tue, 2 Jan 2024 19:51:57 +0100 Subject: [PATCH 026/159] Fix almost all type errors --- src/components/Modal/index.android.tsx | 3 +- src/components/Modal/index.ios.tsx | 3 +- src/components/Modal/index.tsx | 4 +- src/components/Modal/types.ts | 74 +++++++------- src/components/Popover/types.ts | 2 +- .../PopoverWithoutOverlay/index.tsx | 2 +- src/components/ShowContextMenuContext.tsx | 10 +- src/libs/ReportUtils.ts | 6 +- .../BaseReportActionContextMenu.tsx | 23 ++--- .../report/ContextMenu/ContextMenuActions.tsx | 17 ++-- .../PopoverReportActionContextMenu.tsx | 96 ++++++++----------- .../ContextMenu/ReportActionContextMenu.ts | 2 +- 12 files changed, 118 insertions(+), 124 deletions(-) diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 2343cb4c70a9..eb5582e3c2e8 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {AppState} from 'react-native'; -import withWindowDimensions from '@components/withWindowDimensions'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import BaseModal from './BaseModal'; import BaseModalProps from './types'; @@ -28,4 +27,4 @@ function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/index.ios.tsx b/src/components/Modal/index.ios.tsx index f780775ec216..6be171a5de16 100644 --- a/src/components/Modal/index.ios.tsx +++ b/src/components/Modal/index.ios.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import withWindowDimensions from '@components/withWindowDimensions'; import BaseModal from './BaseModal'; import BaseModalProps from './types'; @@ -15,4 +14,4 @@ function Modal({children, ...rest}: BaseModalProps) { } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 4269420dcd7f..8c55f37d4888 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,5 +1,4 @@ import React, {useState} from 'react'; -import withWindowDimensions from '@components/withWindowDimensions'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import StatusBar from '@libs/StatusBar'; @@ -11,6 +10,7 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( const theme = useTheme(); const StyleUtils = useStyleUtils(); const [previousStatusBarColor, setPreviousStatusBarColor] = useState(); + const setStatusBarColor = (color = theme.appBG) => { if (!fullscreen) { @@ -55,4 +55,4 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 461a5935eda9..ccc5db2e7792 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -1,7 +1,6 @@ import {ViewStyle} from 'react-native'; import {ModalProps} from 'react-native-modal'; import {ValueOf} from 'type-fest'; -import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import CONST from '@src/CONST'; type PopoverAnchorPosition = { @@ -11,57 +10,56 @@ type PopoverAnchorPosition = { left?: number; }; -type BaseModalProps = WindowDimensionsProps & - Partial & { - /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ - fullscreen?: boolean; +type BaseModalProps = Partial & { + /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ + fullscreen?: boolean; - /** Should we close modal on outside click */ - shouldCloseOnOutsideClick?: boolean; + /** Should we close modal on outside click */ + shouldCloseOnOutsideClick?: boolean; - /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility?: boolean; + /** Should we announce the Modal visibility changes? */ + shouldSetModalVisibility?: boolean; - /** Callback method fired when the user requests to close the modal */ - onClose: (ref?: React.RefObject) => void; + /** Callback method fired when the user requests to close the modal */ + onClose: () => void; - /** State that determines whether to display the modal or not */ - isVisible: boolean; + /** State that determines whether to display the modal or not */ + isVisible: boolean; - /** Callback method fired when the user requests to submit the modal content. */ - onSubmit?: () => void; + /** Callback method fired when the user requests to submit the modal content. */ + onSubmit?: () => void; - /** Callback method fired when the modal is hidden */ - onModalHide?: () => void; + /** Callback method fired when the modal is hidden */ + onModalHide?: () => void; - /** Callback method fired when the modal is shown */ - onModalShow?: () => void; + /** Callback method fired when the modal is shown */ + onModalShow?: () => void; - /** Style of modal to display */ - type?: ValueOf; + /** Style of modal to display */ + type?: ValueOf; - /** The anchor position of a popover modal. Has no effect on other modal types. */ - popoverAnchorPosition?: PopoverAnchorPosition; + /** The anchor position of a popover modal. Has no effect on other modal types. */ + popoverAnchorPosition?: PopoverAnchorPosition; - outerStyle?: ViewStyle; + outerStyle?: ViewStyle; - /** Whether the modal should go under the system statusbar */ - statusBarTranslucent?: boolean; + /** Whether the modal should go under the system statusbar */ + statusBarTranslucent?: boolean; - /** Whether the modal should avoid the keyboard */ - avoidKeyboard?: boolean; + /** Whether the modal should avoid the keyboard */ + avoidKeyboard?: boolean; - /** Modal container styles */ - innerContainerStyle?: ViewStyle; + /** Modal container styles */ + innerContainerStyle?: ViewStyle; - /** - * Whether the modal should hide its content while animating. On iOS, set to true - * if `useNativeDriver` is also true, to avoid flashes in the UI. - * - * See: https://github.com/react-native-modal/react-native-modal/pull/116 - * */ - hideModalContentWhileAnimating?: boolean; - }; + /** + * Whether the modal should hide its content while animating. On iOS, set to true + * if `useNativeDriver` is also true, to avoid flashes in the UI. + * + * See: https://github.com/react-native-modal/react-native-modal/pull/116 + * */ + hideModalContentWhileAnimating?: boolean; +}; export default BaseModalProps; export type {PopoverAnchorPosition}; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 7f7e2829770c..6a8ae8423d2c 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -13,7 +13,7 @@ type PopoverProps = BaseModalProps & { anchorPosition?: PopoverAnchorPosition; /** The anchor alignment of the popover */ - anchorAlignment: AnchorAlignment; + anchorAlignment?: AnchorAlignment; /** The anchor ref of the popover */ anchorRef: React.RefObject; diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index f83949bcbe9d..dd43c5d332aa 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -51,7 +51,7 @@ function PopoverWithoutOverlay( close: onClose, anchorRef, }); - removeOnClose = Modal.setCloseModal(() => onClose(anchorRef)); + removeOnClose = Modal.setCloseModal(() => onClose()); } else { onModalHide(); close(anchorRef); diff --git a/src/components/ShowContextMenuContext.tsx b/src/components/ShowContextMenuContext.tsx index bbc7abf64e5b..c152289115c8 100644 --- a/src/components/ShowContextMenuContext.tsx +++ b/src/components/ShowContextMenuContext.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import {GestureResponderEvent, Text as RNText} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; @@ -24,7 +25,14 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; * @param checkIfContextMenuActive Callback to update context menu active state * @param isArchivedRoom - Is the report an archived room */ -function showContextMenuForReport(event: Event, anchor: HTMLElement, reportID: string, action: ReportAction, checkIfContextMenuActive: () => void, isArchivedRoom = false) { +function showContextMenuForReport( + event: GestureResponderEvent | MouseEvent, + anchor: RNText | null, + reportID: string, + action: ReportAction, + checkIfContextMenuActive: () => void, + isArchivedRoom = false, +) { if (!DeviceCapabilities.canUseTouchScreen()) { return; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0c1911401432..f67918931200 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4247,7 +4247,7 @@ function navigateToPrivateNotes(report: Report, session: Session) { * - 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: ReportAction, reportID: string) { +function shouldDisableThread(reportAction: OnyxEntry, reportID: string) { const isSplitBillAction = ReportActionsUtils.isSplitBillAction(reportAction); const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); @@ -4255,9 +4255,9 @@ function shouldDisableThread(reportAction: ReportAction, reportID: string) { const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); return ( - CONST.REPORT.ACTIONS.THREAD_DISABLED.some((action: string) => action === reportAction.actionName) || + CONST.REPORT.ACTIONS.THREAD_DISABLED.some((action: string) => action === reportAction?.actionName) || isSplitBillAction || - (isDeletedAction && !reportAction.childVisibleActionCount) || + (isDeletedAction && !reportAction?.childVisibleActionCount) || (isWhisperAction && !isReportPreviewAction && !isIOUAction) || isThreadFirstChat(reportAction, reportID) ); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 96fc5ba1c944..e1e409d24afc 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,5 +1,5 @@ import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, RefObject, useMemo, useRef, useState} from 'react'; +import React, {memo, MutableRefObject, RefObject, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; import ContextMenuItem from '@components/ContextMenuItem'; @@ -14,7 +14,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, ReportAction, ReportActions} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import ContextMenuActions from './ContextMenuActions'; +import ContextMenuActions, {ContextMenuActionPayload} from './ContextMenuActions'; import {ContextMenuType, hideContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { @@ -53,7 +53,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { type?: ContextMenuType; /** Target node which is the target of ContentMenu */ - anchor: HTMLElement; + anchor: MutableRefObject; /** Flag to check if the chat participant is Chronos */ isChronosReport: boolean; @@ -73,8 +73,8 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { function BaseReportActionContextMenu({ type = CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor = null, - contentRef = null, + anchor, + contentRef, isChronosReport = false, isArchivedRoom = false, isMini = false, @@ -161,8 +161,8 @@ function BaseReportActionContextMenu({ > {filteredContextMenuActions.map((contextAction, index) => { const closePopup = !isMini; - const payload = { - reportAction, + const payload: ContextMenuActionPayload = { + reportAction: reportAction as ReportAction, reportID, draftMessage, selection, @@ -173,7 +173,7 @@ function BaseReportActionContextMenu({ if (contextAction.renderContent) { // make sure that renderContent isn't mixed with unsupported props - if (__DEV__ && (contextAction.text != null || contextAction.icon != null)) { + if (__DEV__ && contextAction.icon != null) { throw new Error('Dev error: renderContent() and text/icon cannot be used together.'); } @@ -185,14 +185,15 @@ function BaseReportActionContextMenu({ ref={(ref) => { menuItemRefs.current[index] = ref; }} + // @ts-expect-error TODO: Remove this once ContextMenuItem (https://github.com/Expensify/App/issues/25056) is migrated to TypeScript. icon={contextAction.icon} - text={translate(contextAction.textTranslateKey, {action: reportAction})} + text={contextAction.textTranslateKey ? translate(contextAction.textTranslateKey, {action: reportAction} as never) : undefined} successIcon={contextAction.successIcon} successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} isMini={isMini} key={contextAction.textTranslateKey} - onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)} - description={contextAction.getDescription(selection, isSmallScreenWidth)} + onPress={() => interceptAnonymousUser(() => contextAction.onPress?.(closePopup, payload), contextAction.isAnonymousAction)} + description={contextAction.getDescription?.(selection)} isAnonymousAction={contextAction.isAnonymousAction} isFocused={focusedIndex === index} /> diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 00ccbc4e64ab..4cd407471649 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,5 +1,5 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React from 'react'; +import React, {MutableRefObject} from 'react'; import {OnyxEntry} from 'react-native-onyx'; import {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -50,7 +50,7 @@ type ShouldShow = ( reportAction: OnyxEntry, isArchivedRoom: boolean, betas: OnyxEntry, - menuTarget: HTMLElement, + menuTarget: MutableRefObject, isChronosReport: boolean, reportID: string, isPinnedChat: boolean, @@ -58,7 +58,7 @@ type ShouldShow = ( isOffline: boolean, ) => boolean; -type Payload = { +type ContextMenuActionPayload = { reportAction: ReportAction; reportID: string; draftMessage: string; @@ -68,9 +68,9 @@ type Payload = { interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; }; -type OnPress = (closePopover: boolean, payload: Payload, selection: string | undefined, reportID: string, draftMessage: string) => void; +type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; -type RenderContent = (closePopover: boolean, payload: Payload) => React.ReactElement; +type RenderContent = (closePopover: boolean, payload: ContextMenuActionPayload) => React.ReactElement; type GetDescription = (selection?: string) => string | void; @@ -90,7 +90,7 @@ type ContextMenuAction = { const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, - shouldShow: (type, reportAction) => + shouldShow: (type, reportAction): reportAction is ReportAction => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => { const isMini = !closePopover; @@ -143,7 +143,7 @@ const ContextMenuActions: ContextMenuAction[] = [ icon: Expensicons.Download, successTextTranslateKey: 'common.download', successIcon: Expensicons.Download, - shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline): reportAction is ReportAction => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); const messageHtml = reportAction?.message?.at(0)?.html; return ( @@ -347,7 +347,7 @@ const ContextMenuActions: ContextMenuAction[] = [ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); // Only hide the copylink menu item when context menu is opened over img element. - const isAttachmentTarget = menuTarget?.tagName === 'IMG' && isAttachment; + const isAttachmentTarget = menuTarget.current?.tagName === 'IMG' && isAttachment; return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction); }, onPress: (closePopover, {reportAction, reportID}) => { @@ -504,3 +504,4 @@ const ContextMenuActions: ContextMenuAction[] = [ ]; export default ContextMenuActions; +export type {ContextMenuActionPayload}; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 39e7c66ddb0c..86a7cf7937f3 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,5 +1,5 @@ -import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {Dimensions, EmitterSubscription} from 'react-native'; +import React, {ForwardedRef, forwardRef, RefObject, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {Dimensions, EmitterSubscription, NativeTouchEvent, View} from 'react-native'; import {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; @@ -10,20 +10,30 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; -import {ContextMenuType} from './ReportActionContextMenu'; +import {ContextMenuType, ShowContextMenu} from './ReportActionContextMenu'; + +type HideContextMenu = (onHideActionCallback?: () => void) => void; + +type ShowDeleteModal = (reportID: string, reportAction: ReportAction, shouldSetModalVisibility?: boolean, onConfirm?: () => void, onCancel?: () => void) => void; + +type IsActiveReportAction = (actionID: string) => boolean; type PopoverReportActionContextMenuRef = { - showContextMenu: () => void; - hideContextMenu: () => void; - showDeleteModal: () => void; + showContextMenu: ShowContextMenu; + hideContextMenu: HideContextMenu; + showDeleteModal: ShowDeleteModal; hideDeleteModal: () => void; - isActiveReportAction: () => void; - instanceID: () => void; + isActiveReportAction: IsActiveReportAction; + instanceID: string; runAndResetOnPopoverHide: () => void; clearActiveReportAction: () => void; - contentRef: () => void; + contentRef: RefObject; }; +type ContextMenuAnchorCallback = (x: number, y: number) => void; +type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void}; + +// eslint-disable-next-line @typescript-eslint/naming-convention function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef) { const {translate} = useLocalize(); const reportIDRef = useRef('0'); @@ -32,7 +42,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef(undefined); const cursorRelativePosition = useRef({ horizontal: 0, @@ -56,11 +66,11 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef(null); + const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); - const contextMenuAnchorRef = useRef(null); - const contextMenuTargetNode = useRef(null); + const contextMenuAnchorRef = useRef(null); + const contextMenuTargetNode = useRef(null); const onPopoverShow = useRef(() => {}); const onPopoverHide = useRef(() => {}); @@ -70,16 +80,11 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef {}); const callbackWhenDeleteModalHide = useRef(() => {}); - /** - * Get the Context menu anchor position - * We calculate the achor coordinates from measureInWindow async method - * - * @returns {Promise} - */ + /** Get the Context menu anchor position. We calculate the anchor coordinates from measureInWindow async method */ const getContextMenuMeasuredLocation = useCallback( () => - new Promise((resolve) => { - if (contextMenuAnchorRef.current && _.isFunction(contextMenuAnchorRef.current.measureInWindow)) { + new Promise<{x: number; y: number}>((resolve) => { + if (contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y})); } else { resolve({x: 0, y: 0}); @@ -88,9 +93,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { if (!isPopoverVisible) { return; @@ -120,7 +123,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef !!actionID && (reportActionIDRef.current === actionID || reportActionRef.current.reportActionID === actionID); + const isActiveReportAction: IsActiveReportAction = (actionID) => !!actionID && (reportActionIDRef.current === actionID || reportActionRef.current?.reportActionID === actionID); const clearActiveReportAction = () => { reportActionIDRef.current = '0'; @@ -145,7 +148,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { - const nativeEvent = event.nativeEvent || {}; + const nativeEvent = 'nativeEvent' in event ? event.nativeEvent : ({pageX: 0, pageY: 0} as NativeTouchEvent); contextMenuAnchorRef.current = contextMenuAnchor; contextMenuTargetNode.current = nativeEvent.target; @@ -181,9 +184,9 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { onPopoverShow.current(); @@ -204,19 +205,13 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef {}; }; - /** - * Run the callback and return a noop function to reset it - * @param callback - * @returns - */ - const runAndResetCallback = (callback) => { + /** Run the callback and return a noop function to reset it */ + const runAndResetCallback = (callback: () => void) => { callback(); return () => {}; }; - /** - * After Popover hides, call the registered onPopoverHide & onPopoverHideActionCallback callback and reset it - */ + /** After Popover hides, call the registered onPopoverHide & onPopoverHideActionCallback callback and reset it */ const runAndResetOnPopoverHide = () => { reportIDRef.current = '0'; reportActionIDRef.current = '0'; @@ -230,8 +225,8 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef