diff --git a/android/app/build.gradle b/android/app/build.gradle index aaa6aaeb7a78..ddef060e3a96 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042200 - versionName "1.4.22-0" + versionCode 1001042202 + versionName "1.4.22-2" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ecf3b8c9cad9..1c4a33d37b91 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.22.0 + 1.4.22.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 1ec1aeb6ce14..31fdafe32980 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.22.0 + 1.4.22.2 diff --git a/package-lock.json b/package-lock.json index 2c1da1670e19..065fc0055291 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.22-0", + "version": "1.4.22-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.22-0", + "version": "1.4.22-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 118f63e45879..f6d401a8266b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.22-0", + "version": "1.4.22-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js index 25e1ce6f05ec..231a99f0e6a6 100644 --- a/src/components/AmountTextInput.js +++ b/src/components/AmountTextInput.js @@ -32,7 +32,7 @@ const propTypes = { style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), /** Style for the container */ - containerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + touchableInputWrapperStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), /** Function to call to handle key presses in the text input */ onKeyPress: PropTypes.func, @@ -44,7 +44,7 @@ const defaultProps = { onSelectionChange: () => {}, onKeyPress: () => {}, style: {}, - containerStyles: {}, + touchableInputWrapperStyle: {}, }; function AmountTextInput(props) { @@ -67,7 +67,7 @@ function AmountTextInput(props) { onSelectionChange={props.onSelectionChange} role={CONST.ROLE.PRESENTATION} onKeyPress={props.onKeyPress} - containerStyles={[...StyleUtils.parseStyleAsArray(props.containerStyles)]} + touchableInputWrapperStyle={props.touchableInputWrapperStyle} /> ); } diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index d24d1e18907f..e5b605dd3ff2 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -19,7 +19,6 @@ import fileDownload from '@libs/fileDownload'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import useNativeDriver from '@libs/useNativeDriver'; import reportPropTypes from '@pages/reportPropTypes'; @@ -95,6 +94,9 @@ const propTypes = { /** Whether it is a receipt attachment or not */ isReceiptAttachment: PropTypes.bool, + + /** Whether the receipt can be replaced */ + canEditReceipt: PropTypes.bool, }; const defaultProps = { @@ -113,6 +115,7 @@ const defaultProps = { onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, isReceiptAttachment: false, + canEditReceipt: false, }; function AttachmentModal(props) { @@ -372,13 +375,9 @@ function AttachmentModal(props) { if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) { return []; } - const menuItems = []; - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; - const canEdit = - ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, props.transaction) && - !TransactionUtils.isDistanceRequest(props.transaction); - if (canEdit) { + const menuItems = []; + if (props.canEditReceipt) { menuItems.push({ icon: Expensicons.Camera, text: props.translate('common.replace'), @@ -393,7 +392,7 @@ function AttachmentModal(props) { text: props.translate('common.download'), onSelected: () => downloadAttachment(source), }); - if (TransactionUtils.hasReceipt(props.transaction) && !TransactionUtils.isReceiptBeingScanned(props.transaction) && canEdit) { + if (TransactionUtils.hasReceipt(props.transaction) && !TransactionUtils.isReceiptBeingScanned(props.transaction) && props.canEditReceipt) { menuItems.push({ icon: Expensicons.Trashcan, text: props.translate('receipt.deleteReceipt'), diff --git a/src/components/HeaderPageLayout.js b/src/components/HeaderPageLayout.tsx similarity index 55% rename from src/components/HeaderPageLayout.js rename to src/components/HeaderPageLayout.tsx index 9ef5d4f83a06..304bb2ce49b1 100644 --- a/src/components/HeaderPageLayout.js +++ b/src/components/HeaderPageLayout.tsx @@ -1,56 +1,54 @@ -import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import type {ReactNode} from 'react'; import {ScrollView, View} from 'react-native'; -import _ from 'underscore'; +import type {StyleProp, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import FixedFooter from './FixedFooter'; import HeaderWithBackButton from './HeaderWithBackButton'; -import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes'; +import type HeaderWithBackButtonProps from './HeaderWithBackButton/types'; import ScreenWrapper from './ScreenWrapper'; -const propTypes = { - ...headerWithBackButtonPropTypes, +type HeaderPageLayoutProps = ChildrenProps & + HeaderWithBackButtonProps & { + /** The background color to apply in the upper half of the screen. */ + backgroundColor?: string; - /** Children to display in the lower half of the page (below the header section w/ an animation) */ - children: PropTypes.node.isRequired, + /** A fixed footer to display at the bottom of the page. */ + footer?: ReactNode; - /** The background color to apply in the upper half of the screen. */ - backgroundColor: PropTypes.string, + /** The image to display in the upper half of the screen. */ + headerContent?: ReactNode; - /** A fixed footer to display at the bottom of the page. */ - footer: PropTypes.node, + /** Style to apply to the header image container */ + headerContainerStyles?: StyleProp; - /** The image to display in the upper half of the screen. */ - header: PropTypes.node, + /** Style to apply to the ScrollView container */ + scrollViewContainerStyles?: StyleProp; - /** Style to apply to the header image container */ - // eslint-disable-next-line react/forbid-prop-types - headerContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Style to apply to the children container */ + childrenContainerStyles?: StyleProp; - /** Style to apply to the ScrollView container */ - // eslint-disable-next-line react/forbid-prop-types - scrollViewContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Style to apply to the whole section container */ + style?: StyleProp; + }; - /** Style to apply to the children container */ - // eslint-disable-next-line react/forbid-prop-types - childrenContainerStyles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - backgroundColor: undefined, - header: null, - headerContainerStyles: [], - scrollViewContainerStyles: [], - childrenContainerStyles: [], - footer: null, -}; - -function HeaderPageLayout({backgroundColor, children, footer, headerContainerStyles, scrollViewContainerStyles, childrenContainerStyles, style, headerContent, ...propsToPassToHeader}) { +function HeaderPageLayout({ + backgroundColor, + children, + footer, + headerContainerStyles, + scrollViewContainerStyles, + childrenContainerStyles, + style, + headerContent, + ...rest +}: HeaderPageLayoutProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -58,7 +56,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty const {isOffline} = useNetwork(); const appBGColor = StyleUtils.getBackgroundColorStyle(theme.appBG); const {titleColor, iconFill} = useMemo(() => { - const isColorfulBackground = (backgroundColor || theme.appBG) !== theme.appBG && (backgroundColor || theme.highlightBG) !== theme.highlightBG; + const isColorfulBackground = (backgroundColor ?? theme.appBG) !== theme.appBG && (backgroundColor ?? theme.highlightBG) !== theme.highlightBG; return { titleColor: isColorfulBackground ? theme.textColorfulBackground : undefined, iconFill: isColorfulBackground ? theme.iconColorfulBackground : undefined, @@ -67,7 +65,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty return ( - + {/** Safari on ios/mac has a bug where overscrolling the page scrollview shows green background color. This is a workaround to fix that. https://github.com/Expensify/App/issues/23422 */} {Browser.isSafari() && ( - + )} - - {!Browser.isSafari() && } - + + {!Browser.isSafari() && } + {headerContent} {children} - {!_.isNull(footer) && {footer}} + {!!footer && {footer}} )} @@ -107,8 +102,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty ); } -HeaderPageLayout.propTypes = propTypes; -HeaderPageLayout.defaultProps = defaultProps; HeaderPageLayout.displayName = 'HeaderPageLayout'; +export type {HeaderPageLayoutProps}; export default HeaderPageLayout; diff --git a/src/components/HoldMenuSectionList.tsx b/src/components/HoldMenuSectionList.tsx index 9a9857f037f2..aa5dd75ce159 100644 --- a/src/components/HoldMenuSectionList.tsx +++ b/src/components/HoldMenuSectionList.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import {ImageSourcePropType, View} from 'react-native'; -import {SvgProps} from 'react-native-svg'; +import {View} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; +import type {SvgProps} from 'react-native-svg'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; -import {TranslationPaths} from '@src/languages/types'; +import type {TranslationPaths} from '@src/languages/types'; import Icon from './Icon'; import * as Illustrations from './Icon/Illustrations'; import Text from './Text'; diff --git a/src/components/IllustratedHeaderPageLayout.js b/src/components/IllustratedHeaderPageLayout.js deleted file mode 100644 index 9980d8a7879a..000000000000 --- a/src/components/IllustratedHeaderPageLayout.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import HeaderPageLayout from './HeaderPageLayout'; -import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes'; -import Lottie from './Lottie'; - -const propTypes = { - ...headerWithBackButtonPropTypes, - - /** Children to display in the lower half of the page (below the header section w/ an animation) */ - children: PropTypes.node.isRequired, - - /** The illustration to display in the header. Can be either an SVG component or a JSON object representing a Lottie animation. */ - illustration: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, - - /** The background color to apply in the upper half of the screen. */ - backgroundColor: PropTypes.string, - - /** A fixed footer to display at the bottom of the page. */ - footer: PropTypes.node, - - /** Overlay content to display on top of animation */ - overlayContent: PropTypes.func, -}; - -const defaultProps = { - backgroundColor: undefined, - footer: null, - overlayContent: null, -}; - -function IllustratedHeaderPageLayout({backgroundColor, children, illustration, footer, overlayContent, ...propsToPassToHeader}) { - const theme = useTheme(); - const styles = useThemeStyles(); - return ( - - - {overlayContent && overlayContent()} - - } - headerContainerStyles={[styles.justifyContentCenter, styles.w100]} - footer={footer} - // eslint-disable-next-line react/jsx-props-no-spreading - {...propsToPassToHeader} - > - {children} - - ); -} - -IllustratedHeaderPageLayout.propTypes = propTypes; -IllustratedHeaderPageLayout.defaultProps = defaultProps; -IllustratedHeaderPageLayout.displayName = 'IllustratedHeaderPageLayout'; - -export default IllustratedHeaderPageLayout; diff --git a/src/components/IllustratedHeaderPageLayout.tsx b/src/components/IllustratedHeaderPageLayout.tsx new file mode 100644 index 000000000000..72ec0adf7672 --- /dev/null +++ b/src/components/IllustratedHeaderPageLayout.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type {ReactNode} from 'react'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import HeaderPageLayout from './HeaderPageLayout'; +import type {HeaderPageLayoutProps} from './HeaderPageLayout'; +import Lottie from './Lottie'; +import type DotLottieAnimation from './LottieAnimations/types'; + +type IllustratedHeaderPageLayoutProps = HeaderPageLayoutProps & { + /** The illustration to display in the header. Can be a JSON object representing a Lottie animation. */ + illustration: DotLottieAnimation; + + /** The background color to apply in the upper half of the screen. */ + backgroundColor?: string; + + /** Overlay content to display on top of animation */ + overlayContent?: () => ReactNode; +}; + +function IllustratedHeaderPageLayout({backgroundColor, children, illustration, overlayContent, ...rest}: IllustratedHeaderPageLayoutProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + return ( + + + {overlayContent?.()} + + } + headerContainerStyles={[styles.justifyContentCenter, styles.w100]} + // eslint-disable-next-line react/jsx-props-no-spreading + {...rest} + > + {children} + + ); +} + +IllustratedHeaderPageLayout.displayName = 'IllustratedHeaderPageLayout'; + +export default IllustratedHeaderPageLayout; diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 34d60418d3ab..86e77ae4bfc3 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -35,20 +35,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; @@ -69,170 +55,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?: ViewStyle; - /** 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?: (event: GestureResponderEvent | MouseEvent) => 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( { diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js deleted file mode 100644 index c9eee8e888e1..000000000000 --- a/src/components/MenuItemList.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import useSingleExecution from '@hooks/useSingleExecution'; -import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import MenuItem from './MenuItem'; -import menuItemPropTypes from './menuItemPropTypes'; - -const propTypes = { - /** An array of props that are pass to individual MenuItem components */ - menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), - - /** Whether or not to use the single execution hook */ - shouldUseSingleExecution: PropTypes.bool, -}; -const defaultProps = { - menuItems: [], - shouldUseSingleExecution: false, -}; - -function MenuItemList(props) { - let popoverAnchor; - const {isExecuting, singleExecution} = useSingleExecution(); - - /** - * Handle the secondary interaction for a menu item. - * - * @param {*} link the menu item link or function to get the link - * @param {Event} e the interaction event - */ - const secondaryInteraction = (link, e) => { - if (typeof link === 'function') { - link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, url, popoverAnchor)); - } else if (!_.isEmpty(link)) { - ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, link, popoverAnchor); - } - }; - - return ( - <> - {_.map(props.menuItems, (menuItemProps) => ( - secondaryInteraction(menuItemProps.link, e) : undefined} - ref={(el) => (popoverAnchor = el)} - shouldBlockSelection={Boolean(menuItemProps.link)} - // eslint-disable-next-line react/jsx-props-no-spreading - {...menuItemProps} - disabled={menuItemProps.disabled || isExecuting} - onPress={props.shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} - /> - ))} - - ); -} - -MenuItemList.displayName = 'MenuItemList'; -MenuItemList.propTypes = propTypes; -MenuItemList.defaultProps = defaultProps; - -export default MenuItemList; diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx new file mode 100644 index 000000000000..f83f173a644f --- /dev/null +++ b/src/components/MenuItemList.tsx @@ -0,0 +1,63 @@ +import React, {useRef} from 'react'; +import type {GestureResponderEvent, View} from 'react-native'; +import useSingleExecution from '@hooks/useSingleExecution'; +import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {MenuItemProps} from './MenuItem'; +import MenuItem from './MenuItem'; + +type MenuItemLink = string | (() => Promise); + +type MenuItemWithLink = MenuItemProps & { + /** The link to open when the menu item is clicked */ + link: MenuItemLink; +}; + +type MenuItemListProps = { + /** An array of props that are pass to individual MenuItem components */ + menuItems: MenuItemWithLink[]; + + /** Whether or not to use the single execution hook */ + shouldUseSingleExecution?: boolean; +}; + +function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuItemListProps) { + const popoverAnchor = useRef(null); + const {isExecuting, singleExecution} = useSingleExecution(); + + /** + * Handle the secondary interaction for a menu item. + * + * @param link the menu item link or function to get the link + * @param event the interaction event + */ + const secondaryInteraction = (link: MenuItemLink, event: GestureResponderEvent | MouseEvent) => { + if (typeof link === 'function') { + link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, url, popoverAnchor.current)); + } else if (link) { + ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, link, popoverAnchor.current); + } + }; + + return ( + <> + {menuItems.map((menuItemProps) => ( + secondaryInteraction(menuItemProps.link, e) : undefined} + ref={popoverAnchor} + shouldBlockSelection={!!menuItemProps.link} + // eslint-disable-next-line react/jsx-props-no-spreading + {...menuItemProps} + disabled={!!menuItemProps.disabled || isExecuting} + onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} + /> + ))} + + ); +} + +MenuItemList.displayName = 'MenuItemList'; + +export type {MenuItemWithLink}; +export default MenuItemList; diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 8ed6d0746438..5b59fca6cdae 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -87,9 +87,9 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = lodashGet(policy, 'type'); const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; - const isGroupPolicy = _.contains([CONST.POLICY.TYPE.CORPORATE, CONST.POLICY.TYPE.TEAM], policyType); + const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport); const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID; - const isPayer = isGroupPolicy + const isPayer = isPaidGroupPolicy ? // In a group policy, the admin approver can pay the report directly by skipping the approval step isPolicyAdmin && (isApproved || isManager) : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); @@ -99,11 +99,11 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], ); const shouldShowApproveButton = useMemo(() => { - if (!isGroupPolicy) { + if (!isPaidGroupPolicy) { return false; } return isManager && !isDraft && !isApproved && !isSettled; - }, [isGroupPolicy, isManager, isDraft, isApproved, isSettled]); + }, [isPaidGroupPolicy, isManager, isDraft, isApproved, isSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 7ec95aec951f..2fee67a3d632 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -277,7 +277,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowTags = isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(policyTagList)); // A flag for showing tax rate - const shouldShowTax = isPolicyExpenseChat && policy.isTaxTrackingEnabled; + const shouldShowTax = isPolicyExpenseChat && policy && policy.isTaxTrackingEnabled; // A flag for showing the billable field const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true); diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx index be72fdb98a8b..1b711633ed3b 100644 --- a/src/components/ProcessMoneyRequestHoldMenu.tsx +++ b/src/components/ProcessMoneyRequestHoldMenu.tsx @@ -4,9 +4,9 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Button from './Button'; import HoldMenuSectionList from './HoldMenuSectionList'; -import {PopoverAnchorPosition} from './Modal/types'; +import type {PopoverAnchorPosition} from './Modal/types'; import Popover from './Popover'; -import {AnchorAlignment} from './Popover/types'; +import type {AnchorAlignment} from './Popover/types'; import Text from './Text'; import TextPill from './TextPill'; diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index d425e236431b..c052a885245f 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -210,7 +210,7 @@ function MoneyRequestPreview(props) { } let message = translate('iou.cash'); - if (ReportUtils.isGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) { + if (ReportUtils.isPaidGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) { message += ` • ${translate('iou.approved')}`; } else if (props.iouReport.isWaitingOnBankAccount) { message += ` • ${translate('iou.pending')}`; diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 3437058efa45..37ff163f23c8 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import lodashValues from 'lodash/values'; import PropTypes from 'prop-types'; -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import categoryPropTypes from '@components/categoryPropTypes'; @@ -37,6 +37,7 @@ import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateB import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import iouReportPropTypes from '@pages/iouReportPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; +import {policyDefaultProps, policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -83,6 +84,9 @@ const propTypes = { /** The actions from the parent report */ parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** The policy the report is tied to */ + ...policyPropTypes, + /** Collection of categories attached to a policy */ policyCategories: PropTypes.objectOf(categoryPropTypes), @@ -101,14 +105,15 @@ const propTypes = { const defaultProps = { parentReport: {}, parentReportActions: {}, - policyCategories: {}, transaction: { amount: 0, currency: CONST.CURRENCY.USD, comment: {comment: ''}, }, transactionViolations: [], + policyCategories: {}, policyTags: {}, + ...policyDefaultProps, }; function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy, transactionViolations}) { @@ -147,12 +152,18 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate // Flags for allowing or disallowing editing a money request const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const isCancelled = moneyRequestReport && moneyRequestReport.isCancelledIOU; + + // Used for non-restricted fields such as: description, category, tag, billable, etc. const canEdit = ReportUtils.canEditMoneyRequest(parentReportAction); - const canEditAmount = ReportUtils.canEditMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT, transaction) && !isSettled && !isCardTransaction; - const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, moneyRequestReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT); + const canEditAmount = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT); + const canEditMerchant = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.MERCHANT); + const canEditDate = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); + const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); + const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); // A flag for verifying that the current report is a sub-report of a workspace chat - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); + // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat + const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); // Fetches only the first tag, for now const policyTag = PolicyUtils.getTag(policyTags); @@ -193,18 +204,12 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate } } - // A temporary solution to hide the transaction detail - // This will be removed after we properly add the transaction as a prop - if (ReportActionsUtils.isDeletedAction(parentReportAction)) { - return null; - } - const hasReceipt = TransactionUtils.hasReceipt(transaction); let receiptURIs; let hasErrors = false; if (hasReceipt) { receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction); - hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); + hasErrors = canEditReceipt && TransactionUtils.hasMissingSmartscanFields(transaction); } const pendingAction = lodashGet(transaction, 'pendingAction'); @@ -223,11 +228,12 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate isLocalFile={receiptURIs.isLocalFile} transaction={transaction} enablePreviewModal + canEditReceipt={canEditReceipt} /> )} - {!hasReceipt && canEditReceipt && !isSettled && canUseViolations && ( + {!hasReceipt && canEditReceipt && canUseViolations && ( Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT))} @@ -269,8 +275,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} /> @@ -280,8 +286,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))} brickRoadIndicator={hasViolations('merchant') || (hasErrors && isEmptyMerchant) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} @@ -294,8 +300,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))} brickRoadIndicator={hasViolations('date') || (hasErrors && transactionDate === '') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} diff --git a/src/components/ReportActionItem/ReportActionItemImage.js b/src/components/ReportActionItem/ReportActionItemImage.js index 4336a5eddd8a..1495dcbd9111 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.js +++ b/src/components/ReportActionItem/ReportActionItemImage.js @@ -31,6 +31,9 @@ const propTypes = { /** whether thumbnail is refer the local file or not */ isLocalFile: PropTypes.bool, + + /** whether the receipt can be replaced */ + canEditReceipt: PropTypes.bool, }; const defaultProps = { @@ -38,6 +41,7 @@ const defaultProps = { transaction: {}, enablePreviewModal: false, isLocalFile: false, + canEditReceipt: false, }; /** @@ -46,7 +50,7 @@ const defaultProps = { * and optional preview modal as well. */ -function ReportActionItemImage({thumbnail, image, enablePreviewModal, transaction, isLocalFile}) { +function ReportActionItemImage({thumbnail, image, enablePreviewModal, transaction, canEditReceipt, isLocalFile}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const imageSource = tryResolveUrlFromApiRoot(image || ''); @@ -88,6 +92,7 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal, transactio isAuthTokenRequired={!isLocalFile} report={report} isReceiptAttachment + canEditReceipt={canEditReceipt} allowToDownload originalFileName={transaction.filename} > diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index e88c057a615d..27447a10a32b 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -243,10 +243,10 @@ function ReportPreview(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isGroupPolicy = ReportUtils.isGroupPolicyExpenseChat(props.chatReport); + const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(props.chatReport); const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(props.policy, 'role') === CONST.POLICY.ROLE.ADMIN; - const isPayer = isGroupPolicy - ? // In a group policy, the admin approver can pay the report directly by skipping the approval step + const isPayer = isPaidGroupPolicy + ? // In a paid group policy, the admin approver can pay the report directly by skipping the approval step isPolicyAdmin && (isApproved || isCurrentUserManager) : isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager); const shouldShowPayButton = useMemo( @@ -254,11 +254,11 @@ function ReportPreview(props) { [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, props.iouReport], ); const shouldShowApproveButton = useMemo(() => { - if (!isGroupPolicy) { + if (!isPaidGroupPolicy) { return false; } return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled; - }, [isGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); + }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; return ( diff --git a/src/components/Section/IconSection.js b/src/components/Section/IconSection.js deleted file mode 100644 index 307331aa36d6..000000000000 --- a/src/components/Section/IconSection.js +++ /dev/null @@ -1,41 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import Icon from '@components/Icon'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; -import useThemeStyles from '@hooks/useThemeStyles'; - -const iconSectionPropTypes = { - icon: sourcePropTypes, - IconComponent: PropTypes.IconComponent, - iconContainerStyles: PropTypes.iconContainerStyles, -}; - -const defaultIconSectionPropTypes = { - icon: null, - IconComponent: null, - iconContainerStyles: [], -}; - -function IconSection({icon, IconComponent, iconContainerStyles}) { - const styles = useThemeStyles(); - - return ( - - {Boolean(icon) && ( - - )} - {Boolean(IconComponent) && } - - ); -} - -IconSection.displayName = 'IconSection'; -IconSection.propTypes = iconSectionPropTypes; -IconSection.defaultProps = defaultIconSectionPropTypes; - -export default IconSection; diff --git a/src/components/Section/IconSection.tsx b/src/components/Section/IconSection.tsx new file mode 100644 index 000000000000..cc42c6b7ace5 --- /dev/null +++ b/src/components/Section/IconSection.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type IconSectionProps = { + icon?: IconAsset; + iconContainerStyles?: StyleProp; +}; + +function IconSection({icon, iconContainerStyles}: IconSectionProps) { + const styles = useThemeStyles(); + + return ( + + {!!icon && ( + + )} + + ); +} + +IconSection.displayName = 'IconSection'; + +export default IconSection; diff --git a/src/components/Section/index.js b/src/components/Section/index.js deleted file mode 100644 index 50576abef025..000000000000 --- a/src/components/Section/index.js +++ /dev/null @@ -1,122 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; -import MenuItemList from '@components/MenuItemList'; -import menuItemPropTypes from '@components/menuItemPropTypes'; -import Text from '@components/Text'; -import useThemeStyles from '@hooks/useThemeStyles'; -import IconSection from './IconSection'; - -const CARD_LAYOUT = { - ICON_ON_TOP: 'iconOnTop', - ICON_ON_RIGHT: 'iconOnRight', -}; - -const propTypes = { - /** An array of props that are pass to individual MenuItem components */ - menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), - - /** The text to display in the title of the section */ - title: PropTypes.string.isRequired, - - /** The text to display in the subtitle of the section */ - subtitle: PropTypes.string, - - /** The icon to display along with the title */ - icon: sourcePropTypes, - - /** Icon component */ - IconComponent: PropTypes.func, - - /** Card layout that affects icon positioning, margins, sizes. */ - // eslint-disable-next-line rulesdir/prefer-underscore-method - cardLayout: PropTypes.oneOf(Object.values(CARD_LAYOUT)), - - /** Contents to display inside the section */ - children: PropTypes.node, - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - titleStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - subtitleStyles: PropTypes.arrayOf(PropTypes.object), - - /** Whether the subtitle should have a muted style */ - subtitleMuted: PropTypes.bool, - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - childrenStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the Icon container */ - // eslint-disable-next-line react/forbid-prop-types - iconContainerStyles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - menuItems: null, - children: null, - icon: null, - IconComponent: null, - cardLayout: CARD_LAYOUT.ICON_ON_RIGHT, - containerStyles: [], - iconContainerStyles: [], - titleStyles: [], - subtitleStyles: [], - subtitleMuted: false, - childrenStyles: [], - subtitle: null, -}; - -function Section({children, childrenStyles, containerStyles, icon, IconComponent, cardLayout, iconContainerStyles, menuItems, subtitle, subtitleStyles, subtitleMuted, title, titleStyles}) { - const styles = useThemeStyles(); - - return ( - <> - - {cardLayout === CARD_LAYOUT.ICON_ON_TOP && ( - - )} - - - {title} - - {cardLayout === CARD_LAYOUT.ICON_ON_RIGHT && ( - - )} - - - {Boolean(subtitle) && ( - - {subtitle} - - )} - - {children} - - {Boolean(menuItems) && } - - - ); -} -Section.displayName = 'Section'; -Section.propTypes = propTypes; -Section.defaultProps = defaultProps; - -export {CARD_LAYOUT}; -export default Section; diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx new file mode 100644 index 000000000000..f24316a5f1bb --- /dev/null +++ b/src/components/Section/index.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import type {MenuItemWithLink} from '@components/MenuItemList'; +import MenuItemList from '@components/MenuItemList'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type IconAsset from '@src/types/utils/IconAsset'; +import IconSection from './IconSection'; + +const CARD_LAYOUT = { + ICON_ON_TOP: 'iconOnTop', + ICON_ON_RIGHT: 'iconOnRight', +} as const; + +type SectionProps = ChildrenProps & { + /** An array of props that are passed to individual MenuItem components */ + menuItems?: MenuItemWithLink[]; + + /** The text to display in the title of the section */ + title: string; + + /** The text to display in the subtitle of the section */ + subtitle?: string; + + /** The icon to display along with the title */ + icon?: IconAsset; + + /** Card layout that affects icon positioning, margins, sizes */ + cardLayout?: ValueOf; + + /** Whether the subtitle should have a muted style */ + subtitleMuted?: boolean; + + /** Customize the Section container */ + containerStyles?: StyleProp; + + /** Customize the Section container */ + titleStyles?: StyleProp; + + /** Customize the Section container */ + subtitleStyles?: StyleProp; + + /** Customize the Section container */ + childrenStyles?: StyleProp; + + /** Customize the Icon container */ + iconContainerStyles?: StyleProp; +}; + +function Section({ + children, + childrenStyles, + containerStyles, + icon, + cardLayout = CARD_LAYOUT.ICON_ON_RIGHT, + iconContainerStyles, + menuItems, + subtitle, + subtitleStyles, + subtitleMuted = false, + title, + titleStyles, +}: SectionProps) { + const styles = useThemeStyles(); + + return ( + <> + + {cardLayout === CARD_LAYOUT.ICON_ON_TOP && ( + + )} + + + {title} + + {cardLayout === CARD_LAYOUT.ICON_ON_RIGHT && ( + + )} + + + {!!subtitle && ( + + {subtitle} + + )} + + {children} + + {!!menuItems && } + + + ); +} +Section.displayName = 'Section'; + +export {CARD_LAYOUT}; +export default Section; diff --git a/src/components/TabSelector/TabLabel.tsx b/src/components/TabSelector/TabLabel.tsx index 40f4dc30bb97..548b4ebccbc8 100644 --- a/src/components/TabSelector/TabLabel.tsx +++ b/src/components/TabSelector/TabLabel.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {Animated, StyleSheet, Text, View} from 'react-native'; +import {Animated, StyleSheet, View} from 'react-native'; +import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; type TabLabelProps = { diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index 78c37e94196a..78f06b4075e0 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -26,6 +26,9 @@ const propTypes = { /** Customize the TextInput container */ textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), + /** Customizes the touchable wrapper of the TextInput component */ + touchableInputWrapperStyle: PropTypes.arrayOf(PropTypes.object), + /** Customize the main container */ containerStyles: PropTypes.arrayOf(PropTypes.object), diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index d548041b0cf8..9c3899979aaa 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -37,6 +37,7 @@ function BaseTextInput( errorText = '', icon = null, textInputContainerStyles, + touchableInputWrapperStyle, containerStyles, inputStyle, forceActiveLabel = false, @@ -287,7 +288,7 @@ function BaseTextInput( style={[ autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, - containerStyles, + touchableInputWrapperStyle, ]} > ; + /** Customizes the touchable wrapper of the TextInput component */ + touchableInputWrapperStyle?: StyleProp; + /** Customize the main container */ containerStyles?: StyleProp; diff --git a/src/components/TextPill.tsx b/src/components/TextPill.tsx index 035ae1dd42d8..6d473b189534 100644 --- a/src/components/TextPill.tsx +++ b/src/components/TextPill.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {StyleProp, TextStyle} from 'react-native'; +import type {StyleProp, TextStyle} from 'react-native'; // eslint-disable-next-line no-restricted-imports import useThemeStyles from '@hooks/useThemeStyles'; import colors from '@styles/theme/colors'; diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 20f52e2d08ee..a9b4566a390c 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -469,7 +469,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { setSelectionHour(e.nativeEvent.selection); }} style={styles.timePickerInput} - containerStyles={[styles.timePickerHeight100]} + touchableInputWrapperStyle={styles.timePickerHeight100} selection={selectionHour} showSoftInputOnFocus={false} /> @@ -497,7 +497,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { setSelectionMinute(e.nativeEvent.selection); }} style={styles.timePickerInput} - containerStyles={[styles.timePickerHeight100]} + touchableInputWrapperStyle={styles.timePickerHeight100} selection={selectionMinute} showSoftInputOnFocus={false} /> diff --git a/src/hooks/useSingleExecution/index.ts b/src/hooks/useSingleExecution/index.ts index f1be359f0355..909416dd848b 100644 --- a/src/hooks/useSingleExecution/index.ts +++ b/src/hooks/useSingleExecution/index.ts @@ -9,9 +9,9 @@ type Action = (...params: T) => void | Promise; */ export default function useSingleExecution() { const singleExecution = useCallback( - (action: Action) => + (action?: Action) => (...params: T) => { - action(...params); + action?.(...params); }, [], ); diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index dbb2790c1318..7721a64adea9 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import {createStackNavigator} from '@react-navigation/stack'; -import React, {useMemo} from 'react'; +import React, {useMemo, useRef} from 'react'; import {View} from 'react-native'; import NoDropZone from '@components/DragAndDrop/NoDropZone'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,10 +20,21 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) { const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]); + const isExecutingRef = useRef(false); return ( - {!isSmallScreenWidth && } + {!isSmallScreenWidth && ( + { + if (isExecutingRef.current) { + return; + } + isExecutingRef.current = true; + navigation.goBack(); + }} + /> + )} | EmptyObject): boolean { /** * Checks if a report is an IOU report. */ -function isIOUReport(report: OnyxEntry): boolean { +function isIOUReport(reportOrID: OnyxEntry | string | EmptyObject): boolean { + const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return report?.type === CONST.REPORT.TYPE.IOU; } @@ -602,14 +603,15 @@ function isReportManager(report: OnyxEntry): boolean { /** * Checks if the supplied report has been approved */ -function isReportApproved(report: OnyxEntry | EmptyObject): boolean { +function isReportApproved(reportOrID: OnyxEntry | string | EmptyObject): boolean { + const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; } /** * Checks if the supplied report is an expense report in Open state and status. */ -function isDraftExpenseReport(report: OnyxEntry): boolean { +function isDraftExpenseReport(report: OnyxEntry | EmptyObject): boolean { return isExpenseReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } @@ -716,9 +718,17 @@ function isControlPolicyExpenseChat(report: OnyxEntry): boolean { } /** - * Whether the provided report belongs to a Control or Collect policy + * Whether the provided report belongs to a Free, Collect or Control policy */ function isGroupPolicy(report: OnyxEntry): boolean { + const policyType = getPolicyType(report, allPolicies); + return policyType === CONST.POLICY.TYPE.CORPORATE || policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.FREE; +} + +/** + * Whether the provided report belongs to a Control or Collect policy + */ +function isPaidGroupPolicy(report: OnyxEntry): boolean { const policyType = getPolicyType(report, allPolicies); return policyType === CONST.POLICY.TYPE.CORPORATE || policyType === CONST.POLICY.TYPE.TEAM; } @@ -726,8 +736,8 @@ function isGroupPolicy(report: OnyxEntry): boolean { /** * Whether the provided report belongs to a Control or Collect policy and is an expense chat */ -function isGroupPolicyExpenseChat(report: OnyxEntry): boolean { - return isPolicyExpenseChat(report) && isGroupPolicy(report); +function isPaidGroupPolicyExpenseChat(report: OnyxEntry): boolean { + return isPolicyExpenseChat(report) && isPaidGroupPolicy(report); } /** @@ -740,8 +750,8 @@ function isControlPolicyExpenseReport(report: OnyxEntry): boolean { /** * Whether the provided report belongs to a Control or Collect policy and is an expense report */ -function isGroupPolicyExpenseReport(report: OnyxEntry): boolean { - return isExpenseReport(report) && isGroupPolicy(report); +function isPaidGroupPolicyExpenseReport(report: OnyxEntry): boolean { + return isExpenseReport(report) && isPaidGroupPolicy(report); } /** @@ -819,7 +829,7 @@ function isConciergeChatReport(report: OnyxEntry): boolean { /** * Returns true if report is still being processed */ -function isProcessingReport(report: OnyxEntry): boolean { +function isProcessingReport(report: OnyxEntry | EmptyObject): boolean { return report?.stateNum === CONST.REPORT.STATE_NUM.PROCESSING && report?.statusNum === CONST.REPORT.STATUS.SUBMITTED; } @@ -1850,9 +1860,13 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF * - the current user is the requestor and is not settled yet * - in case of expense report * - the current user is the requestor and is not settled yet - * - or the user is an admin on the policy the expense report is tied to + * - the current user is the manager of the report + * - or the current user is an admin on the policy the expense report is tied to + * + * This is used in conjunction with canEditRestrictedField to control editing of specific fields like amount, currency, created, receipt, and distance. + * On its own, it only controls allowing/disallowing navigating to the editing pages or showing/hiding the 'Edit' icon on report actions */ -function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit = '', transaction?: OnyxEntry): boolean { +function canEditMoneyRequest(reportAction: OnyxEntry): boolean { const isDeleted = ReportActionsUtils.isDeletedAction(reportAction); if (isDeleted) { @@ -1875,55 +1889,76 @@ function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit } const moneyRequestReport = getReport(String(moneyRequestReportID)); - const isReportSettled = isSettled(moneyRequestReport?.reportID); - const isApproved = isReportApproved(moneyRequestReport); - const isAdmin = isExpenseReport(moneyRequestReport) && (getPolicy(moneyRequestReport?.policyID ?? '')?.role ?? '') === CONST.POLICY.ROLE.ADMIN; const isRequestor = currentUserAccountID === reportAction?.actorAccountID; - const isDistanceRequest = !isEmpty(transaction) && TransactionUtils.isDistanceRequest(transaction); - if (isAdmin && !isRequestor && fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { - return false; - } - if (isDistanceRequest && fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) { - return isAdmin; + if (isIOUReport(moneyRequestReport)) { + return isProcessingReport(moneyRequestReport) && isRequestor; } - if (isAdmin) { + + const policy = getPolicy(moneyRequestReport?.policyID ?? ''); + const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; + const isManager = currentUserAccountID === moneyRequestReport?.managerID; + + // Admin & managers can always edit coding fields such as tag, category, billable, etc. As long as the report has a state higher than OPEN. + if ((isAdmin || isManager) && !isDraftExpenseReport(moneyRequestReport)) { return true; } - return !isApproved && !isReportSettled && isRequestor; + return !isReportApproved(moneyRequestReport) && !isSettled(moneyRequestReport?.reportID) && isRequestor; } /** * Checks if the current user can edit the provided property of a money request * */ -function canEditFieldOfMoneyRequest( - reportAction: OnyxEntry, - reportID: string, - fieldToEdit: ValueOf, - transaction: OnyxEntry, -): boolean { +function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, fieldToEdit: ValueOf): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled - const nonEditableFieldsWhenSettled: string[] = [ + const restrictedFields: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, CONST.EDIT_REQUEST_FIELD.CURRENCY, + CONST.EDIT_REQUEST_FIELD.MERCHANT, CONST.EDIT_REQUEST_FIELD.DATE, CONST.EDIT_REQUEST_FIELD.RECEIPT, CONST.EDIT_REQUEST_FIELD.DISTANCE, ]; - // Checks if this user has permissions to edit this money request - if (!canEditMoneyRequest(reportAction, fieldToEdit, transaction)) { - return false; // User doesn't have permission to edit + if (!canEditMoneyRequest(reportAction)) { + return false; + } + + // If we're editing fields such as category, tag, description, etc. the check above should be enough for handling the permission + if (!restrictedFields.includes(fieldToEdit)) { + return true; } - if (!isEmpty(transaction) && fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT && TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + + const iouMessage = reportAction?.originalMessage as IOUMessage; + const moneyRequestReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouMessage?.IOUReportID}`] ?? ({} as Report); + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${iouMessage?.IOUTransactionID}`] ?? ({} as Transaction); + + if (isSettled(String(moneyRequestReport.reportID)) || isReportApproved(String(moneyRequestReport.reportID))) { return false; } - // Checks if the report is settled - // Checks if the provided property is a restricted one - return !isSettled(reportID) || !nonEditableFieldsWhenSettled.includes(fieldToEdit); + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT || fieldToEdit === CONST.EDIT_REQUEST_FIELD.CURRENCY) { + if (TransactionUtils.isCardTransaction(transaction)) { + return false; + } + + if (TransactionUtils.isDistanceRequest(transaction)) { + const policy = getPolicy(moneyRequestReport?.reportID ?? ''); + const isAdmin = isExpenseReport(moneyRequestReport) && policy.role === CONST.POLICY.ROLE.ADMIN; + const isManager = isExpenseReport(moneyRequestReport) && currentUserAccountID === moneyRequestReport?.managerID; + + return isAdmin || isManager; + } + } + + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { + const isRequestor = currentUserAccountID === reportAction?.actorAccountID; + return !TransactionUtils.isReceiptBeingScanned(transaction) && !TransactionUtils.isDistanceRequest(transaction) && isRequestor; + } + + return true; } /** @@ -2063,7 +2098,7 @@ function getReportPreviewMessage( const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency); - if (isReportApproved(report) && isGroupPolicy(report)) { + if (isReportApproved(report) && isPaidGroupPolicy(report)) { return Localize.translateLocal('iou.managerApprovedAmount', { manager: payerName ?? '', amount: formattedAmount, @@ -4347,10 +4382,12 @@ export { formatReportLastMessageText, chatIncludesConcierge, isPolicyExpenseChat, + isGroupPolicy, + isPaidGroupPolicy, isControlPolicyExpenseChat, isControlPolicyExpenseReport, - isGroupPolicyExpenseChat, - isGroupPolicyExpenseReport, + isPaidGroupPolicyExpenseChat, + isPaidGroupPolicyExpenseReport, getIconsForParticipants, getIcons, getRoomWelcomeMessage, diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 2291e6d0af4a..a0772db49585 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -7,6 +7,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer, Request} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as QueuedOnyxUpdates from './QueuedOnyxUpdates'; // This key needs to be separate from ONYXKEYS.ONYX_UPDATES_FROM_SERVER so that it can be updated without triggering the callback when the server IDs are updated. If that @@ -74,7 +75,18 @@ function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFrom Log.info(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, false, {command: request?.command}); if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) <= lastUpdateIDAppliedToClient) { - Log.info('[OnyxUpdateManager] Update received was older or the same than current state, returning without applying the updates', false); + Log.info('[OnyxUpdateManager] Update received was older than or the same as current state, returning without applying the updates other than successData and failureData'); + + // In this case, we're already received the OnyxUpdate included in the response, so we don't need to apply it again. + // However, we do need to apply the successData and failureData from the request + if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response && (!isEmptyObject(request.successData) || !isEmptyObject(request.failureData))) { + Log.info('[OnyxUpdateManager] Applying success or failure data from request without onyxData from response'); + + // We use a spread here instead of delete because we don't want to change the response for other middlewares + const {onyxData, ...responseWithoutOnyxData} = response; + return applyHTTPSOnyxUpdates(request, responseWithoutOnyxData); + } + return Promise.resolve(); } if (lastUpdateID && (lastUpdateIDAppliedToClient === null || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 043f99265b5f..cef236a3e1bb 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2120,6 +2120,24 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal }, ]; + if (report.parentReportID && report.parentReportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}}, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: report.notificationPreference}}, + }); + } + type LeaveRoomParameters = { reportID: string; }; diff --git a/src/libs/updatePropsPaperWorklet/index.js b/src/libs/updatePropsPaperWorklet/index.js deleted file mode 100644 index 1bca6ea13cdc..000000000000 --- a/src/libs/updatePropsPaperWorklet/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function () { - 'worklet'; -} diff --git a/src/libs/updatePropsPaperWorklet/index.native.js b/src/libs/updatePropsPaperWorklet/index.native.js deleted file mode 100644 index ed79b38ffab5..000000000000 --- a/src/libs/updatePropsPaperWorklet/index.native.js +++ /dev/null @@ -1,13 +0,0 @@ -export default function (viewTag, viewName, updates) { - 'worklet'; - - // _updatePropsPaper is a function that is worklet function from react-native-reanimated which is not available on web - // eslint-disable-next-line no-undef - _updatePropsPaper([ - { - tag: viewTag, - name: viewName, - updates, - }, - ]); -} diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 54c5202fb205..bc05c110ab2f 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import lodashValues from 'lodash/values'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import categoryPropTypes from '@components/categoryPropTypes'; @@ -47,9 +47,6 @@ const propTypes = { /** The report object for the thread report */ report: reportPropTypes, - /** The parent report object for the thread report */ - parentReport: reportPropTypes, - /** Collection of categories attached to a policy */ policyCategories: PropTypes.objectOf(categoryPropTypes), @@ -65,14 +62,13 @@ const propTypes = { const defaultProps = { report: {}, - parentReport: {}, policyCategories: {}, policyTags: {}, parentReportActions: {}, transaction: {}, }; -function EditRequestPage({report, route, parentReport, policyCategories, policyTags, parentReportActions, transaction}) { +function EditRequestPage({report, route, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); const { @@ -93,7 +89,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT const tagListName = PolicyUtils.getTagListName(policyTags); // A flag for verifying that the current report is a sub-report of a workspace chat - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); + const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); // A flag for showing the categories page const shouldShowCategories = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories))); @@ -104,7 +100,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT // Decides whether to allow or disallow editing a money request useEffect(() => { // Do not dismiss the modal, when a current user can edit this property of the money request. - if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, parentReport.reportID, fieldToEdit, transaction)) { + if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, fieldToEdit)) { return; } @@ -112,7 +108,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT Navigation.isNavigationReady().then(() => { Navigation.dismissModal(); }); - }, [parentReportAction, parentReport.reportID, fieldToEdit, transaction]); + }, [parentReportAction, fieldToEdit]); // Update the transaction object and close the modal function editMoneyRequest(transactionChanges) { @@ -303,9 +299,6 @@ export default compose( policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report ? report.parentReportID : '0'}`, - }, parentReportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, canEvict: false, diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 76af9d4fccb0..5b64d90da5da 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -18,7 +18,7 @@ type ShowContextMenu = ( type: ContextMenuType, event: GestureResponderEvent | MouseEvent, selection: string, - contextMenuAnchor: RNText | null, + contextMenuAnchor: View | RNText | null, reportID?: string, reportActionID?: string, originalReportID?: string, @@ -96,7 +96,7 @@ function showContextMenu( type: ContextMenuType, event: GestureResponderEvent | MouseEvent, selection: string, - contextMenuAnchor: RNText | null, + contextMenuAnchor: View | RNText | null, reportID = '0', reportActionID = '0', originalReportID = '0', diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index da79db6f2ec8..c072666920ae 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import {runOnJS, useAnimatedRef} from 'react-native-reanimated'; +import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -24,7 +24,6 @@ import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getModalState from '@libs/getModalState'; import * as ReportUtils from '@libs/ReportUtils'; -import updatePropsPaperWorklet from '@libs/updatePropsPaperWorklet'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; @@ -348,13 +347,10 @@ function ReportActionCompose({ return; } - const viewTag = animatedRef(); - const viewName = 'RCTMultilineTextInputView'; - const updates = {text: ''}; // We are setting the isCommentEmpty flag to true so the status of it will be in sync of the native text input state runOnJS(setIsCommentEmpty)(true); runOnJS(resetFullComposerSize)(); - updatePropsPaperWorklet(viewTag, viewName, updates); // clears native text input on the UI thread + setNativeProps(animatedRef, {text: ''}); // clears native text input on the UI thread runOnJS(submitForm)(); }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index eab9ab5a7510..24833fc96fdc 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -86,7 +86,7 @@ function IOUCurrencySelection(props) { const parentReportAction = ReportActionsUtils.getReportAction(report.parentReportID, report.parentReportActionID); // Do not dismiss the modal, when a current user can edit this currency of this money request. - if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, report.parentReportID, CONST.EDIT_REQUEST_FIELD.CURRENCY)) { + if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.CURRENCY)) { return; } diff --git a/src/styles/index.ts b/src/styles/index.ts index 4ef8740fbebb..6d3cbd93c6c8 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3708,7 +3708,8 @@ const styles = (theme: ThemeColors) => fontFamily: isSelected ? FontUtils.fontFamily.platform.EXP_NEUE_BOLD : FontUtils.fontFamily.platform.EXP_NEUE, fontWeight: isSelected ? FontUtils.fontWeight.bold : '400', color: isSelected ? theme.text : theme.textSupporting, - lineHeight: 14, + lineHeight: variables.lineHeightNormal, + fontSize: variables.fontSizeNormal, } satisfies TextStyle), tabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation) => ({ diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 80834c9a0261..08a89526e4c3 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -142,7 +142,7 @@ export default { signInLogoWidthLargeScreen: 144, signInLogoHeightLargeScreen: 108, signInLogoWidthPill: 132, - tabSelectorButtonHeight: 40, + tabSelectorButtonHeight: 42, tabSelectorButtonPadding: 12, lhnLogoWidth: 95.09, lhnLogoHeight: 22.33, diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index a82903762631..88f3fe99b347 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -153,7 +153,7 @@ function ReportScreenWrapper(args) { const runs = CONST.PERFORMANCE_TESTS.RUNS; -test('[ReportScreen] should render ReportScreen with composer interactions', () => { +test.skip('[ReportScreen] should render ReportScreen with composer interactions', () => { const {triggerTransitionEnd, addListener} = createAddListenerMock(); const scenario = async () => { /** @@ -226,7 +226,7 @@ test('[ReportScreen] should render ReportScreen with composer interactions', () ); }); -test('[ReportScreen] should press of the report item', () => { +test.skip('[ReportScreen] should press of the report item', () => { const {triggerTransitionEnd, addListener} = createAddListenerMock(); const scenario = async () => { /**