diff --git a/src/components/MessagesRow.js b/src/components/MessagesRow.tsx similarity index 60% rename from src/components/MessagesRow.js rename to src/components/MessagesRow.tsx index e4d6240ba0fd..02b78942dfcf 100644 --- a/src/components/MessagesRow.js +++ b/src/components/MessagesRow.tsx @@ -1,55 +1,45 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {StyleProp, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; +import * as Localize from '@libs/Localize'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DotIndicatorMessage from './DotIndicatorMessage'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip'; -const propTypes = { +type MessagesRowProps = { /** The messages to display */ - messages: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.oneOfType([PropTypes.string, PropTypes.object]), PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - ), + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ - type: PropTypes.oneOf(['error', 'success']).isRequired, + type: 'error' | 'success'; /** A function to run when the X button next to the message is clicked */ - onClose: PropTypes.func, + onClose?: () => void; /** Additional style object for the container */ - containerStyles: stylePropTypes, + containerStyles?: StyleProp; /** Whether we can dismiss the messages */ - canDismiss: PropTypes.bool, + canDismiss?: boolean; }; -const defaultProps = { - messages: {}, - onClose: () => {}, - containerStyles: [], - canDismiss: true, -}; - -function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) { +function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles, canDismiss = true}: MessagesRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - if (_.isEmpty(messages)) { + + if (isEmptyObject(messages)) { return null; } return ( - + @@ -69,8 +59,6 @@ function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) { ); } -MessagesRow.propTypes = propTypes; -MessagesRow.defaultProps = defaultProps; MessagesRow.displayName = 'MessagesRow'; export default MessagesRow; diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js deleted file mode 100644 index 7a2dfbd2b6da..000000000000 --- a/src/components/OfflineWithFeedback.js +++ /dev/null @@ -1,143 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import useNetwork from '@hooks/useNetwork'; -import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; -import useThemeStyles from '@styles/useThemeStyles'; -import CONST from '@src/CONST'; -import MessagesRow from './MessagesRow'; - -/** - * This component should be used when we are using the offline pattern B (offline with feedback). - * You should enclose any element that should have feedback that the action was taken offline and it will take - * care of adding the appropriate styles for pending actions and displaying the dismissible error. - */ - -const propTypes = { - /** The type of action that's pending */ - pendingAction: PropTypes.oneOf(['add', 'update', 'delete']), - - /** Determine whether to hide the component's children if deletion is pending */ - shouldHideOnDelete: PropTypes.bool, - - /** The errors to display */ - // eslint-disable-next-line react/forbid-prop-types - errors: PropTypes.object, - - /** Whether we should show the error messages */ - shouldShowErrorMessages: PropTypes.bool, - - /** Whether we should disable opacity */ - shouldDisableOpacity: PropTypes.bool, - - /** A function to run when the X button next to the error is clicked */ - onClose: PropTypes.func, - - /** The content that needs offline feedback */ - children: PropTypes.node.isRequired, - - /** Additional styles to add after local styles. Applied to the parent container */ - style: stylePropTypes, - - /** Additional styles to add after local styles. Applied to the children wrapper container */ - contentContainerStyle: stylePropTypes, - - /** Additional style object for the error row */ - errorRowStyles: stylePropTypes, - - /** Whether applying strikethrough to the children should be disabled */ - shouldDisableStrikeThrough: PropTypes.bool, - - /** Whether to apply needsOffscreenAlphaCompositing prop to the children */ - needsOffscreenAlphaCompositing: PropTypes.bool, - - /** Whether we can dismiss the error message */ - canDismissError: PropTypes.bool, -}; - -const defaultProps = { - pendingAction: null, - shouldHideOnDelete: true, - errors: null, - shouldShowErrorMessages: true, - shouldDisableOpacity: false, - onClose: () => {}, - style: [], - contentContainerStyle: [], - errorRowStyles: [], - shouldDisableStrikeThrough: false, - needsOffscreenAlphaCompositing: false, - canDismissError: true, -}; - -/** - * This method applies the strikethrough to all the children passed recursively - * @param {Array} children - * @param {Object} styles - * @return {Array} - */ -function applyStrikeThrough(children, styles) { - return React.Children.map(children, (child) => { - if (!React.isValidElement(child)) { - return child; - } - const props = {style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted, styles.userSelectNone)}; - if (child.props.children) { - props.children = applyStrikeThrough(child.props.children, styles); - } - return React.cloneElement(child, props); - }); -} - -function OfflineWithFeedback(props) { - const styles = useThemeStyles(); - const {isOffline} = useNetwork(); - - const hasErrors = !_.isEmpty(props.errors); - - // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = _.omit(props.errors, (e) => e === null); - const hasErrorMessages = !_.isEmpty(errorMessages); - const isOfflinePendingAction = isOffline && props.pendingAction; - const isUpdateOrDeleteError = hasErrors && (props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - const isAddError = hasErrors && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; - const needsOpacity = !props.shouldDisableOpacity && ((isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError); - const needsStrikeThrough = !props.shouldDisableStrikeThrough && isOffline && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - const hideChildren = props.shouldHideOnDelete && !isOffline && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !hasErrors; - let children = props.children; - - // Apply strikethrough to children if needed, but skip it if we are not going to render them - if (needsStrikeThrough && !hideChildren) { - children = applyStrikeThrough(children, styles); - } - return ( - - {!hideChildren && ( - - {children} - - )} - {props.shouldShowErrorMessages && hasErrorMessages && ( - - )} - - ); -} - -OfflineWithFeedback.propTypes = propTypes; -OfflineWithFeedback.defaultProps = defaultProps; -OfflineWithFeedback.displayName = 'OfflineWithFeedback'; - -export default OfflineWithFeedback; diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx new file mode 100644 index 000000000000..17772e148f53 --- /dev/null +++ b/src/components/OfflineWithFeedback.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; +import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; +import * as StyleUtils from '@styles/StyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; +import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import MessagesRow from './MessagesRow'; + +/** + * This component should be used when we are using the offline pattern B (offline with feedback). + * You should enclose any element that should have feedback that the action was taken offline and it will take + * care of adding the appropriate styles for pending actions and displaying the dismissible error. + */ + +type OfflineWithFeedbackProps = ChildrenProps & { + /** The type of action that's pending */ + pendingAction: OnyxCommon.PendingAction; + + /** Determine whether to hide the component's children if deletion is pending */ + shouldHideOnDelete?: boolean; + + /** The errors to display */ + errors?: OnyxCommon.Errors; + + /** Whether we should show the error messages */ + shouldShowErrorMessages?: boolean; + + /** Whether we should disable opacity */ + shouldDisableOpacity?: boolean; + + /** A function to run when the X button next to the error is clicked */ + onClose?: () => void; + + /** Additional styles to add after local styles. Applied to the parent container */ + style?: StyleProp; + + /** Additional styles to add after local styles. Applied to the children wrapper container */ + contentContainerStyle?: StyleProp; + + /** Additional style object for the error row */ + errorRowStyles?: StyleProp; + + /** Whether applying strikethrough to the children should be disabled */ + shouldDisableStrikeThrough?: boolean; + + /** Whether to apply needsOffscreenAlphaCompositing prop to the children */ + needsOffscreenAlphaCompositing?: boolean; + + /** Whether we can dismiss the error message */ + canDismissError?: boolean; +}; + +type StrikethroughProps = Partial & {style: Array}; + +/** + * This method applies the strikethrough to all the children passed recursively + */ +function applyStrikeThrough(children: React.ReactNode, styles: ReturnType): React.ReactNode { + return React.Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child; + } + + const props: StrikethroughProps = { + style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted, styles.userSelectNone), + }; + + if (child.props.children) { + props.children = applyStrikeThrough(child.props.children, styles); + } + + return React.cloneElement(child, props); + }); +} + +function omitBy(obj: Record | undefined, predicate: (value: T) => boolean) { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars + return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, value]) => !predicate(value))); +} + +function OfflineWithFeedback({ + pendingAction, + canDismissError = true, + contentContainerStyle, + errorRowStyles, + errors, + needsOffscreenAlphaCompositing = false, + onClose = () => {}, + shouldDisableOpacity = false, + shouldDisableStrikeThrough = false, + shouldHideOnDelete = true, + shouldShowErrorMessages = true, + style, + ...rest +}: OfflineWithFeedbackProps) { + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + const hasErrors = isNotEmptyObject(errors ?? {}); + // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. + const errorMessages = omitBy(errors, (e) => e === null); + const hasErrorMessages = isNotEmptyObject(errorMessages); + const isOfflinePendingAction = !!isOffline && !!pendingAction; + const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + const needsOpacity = !shouldDisableOpacity && ((isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError); + const needsStrikeThrough = !shouldDisableStrikeThrough && isOffline && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const hideChildren = shouldHideOnDelete && !isOffline && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !hasErrors; + let children = rest.children; + + // Apply strikethrough to children if needed, but skip it if we are not going to render them + if (needsStrikeThrough && !hideChildren) { + children = applyStrikeThrough(children, styles); + } + return ( + + {!hideChildren && ( + + {children} + + )} + {shouldShowErrorMessages && hasErrorMessages && ( + + )} + + ); +} + +OfflineWithFeedback.displayName = 'OfflineWithFeedback'; + +export default OfflineWithFeedback;