From 04ef83175c2f164d052b14f490d7d2c77ce5b2c5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 9 Nov 2023 15:35:53 +0100 Subject: [PATCH 1/8] ref: migrate OfflineWithFeedback to TS --- src/components/OfflineWithFeedback.js | 141 ------------------------- src/components/OfflineWithFeedback.tsx | 136 ++++++++++++++++++++++++ src/types/utils/EmptyObject.ts | 15 +++ src/types/utils/Falsy.ts | 3 + 4 files changed, 154 insertions(+), 141 deletions(-) delete mode 100644 src/components/OfflineWithFeedback.js create mode 100644 src/components/OfflineWithFeedback.tsx create mode 100644 src/types/utils/EmptyObject.ts create mode 100644 src/types/utils/Falsy.ts diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js deleted file mode 100644 index ba893aeb2fe4..000000000000 --- a/src/components/OfflineWithFeedback.js +++ /dev/null @@ -1,141 +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 styles from '@styles/styles'; -import * as StyleUtils from '@styles/StyleUtils'; -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 - * @return {Array} - */ -function applyStrikeThrough(children) { - 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); - } - return React.cloneElement(child, props); - }); -} - -function OfflineWithFeedback(props) { - 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); - } - 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..3fe7fd99633f --- /dev/null +++ b/src/components/OfflineWithFeedback.tsx @@ -0,0 +1,136 @@ +import lodashOmitBy from 'lodash/omitBy'; +import React from 'react'; +import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; +import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; +import styles from '@styles/styles'; +import * as StyleUtils from '@styles/StyleUtils'; +import CONST from '@src/CONST'; +import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} 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 = { + /** The type of action that's pending */ + pendingAction: OnyxCommon.PendingAction | null; + + /** Determine whether to hide the component's children if deletion is pending */ + shouldHideOnDelete?: boolean; + + /** The errors to display */ + errors?: OnyxCommon.Errors | null; + + /** 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; + + /** The content that needs offline feedback */ + children: React.ReactNode; + + /** 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; +}; + +/** + * This method applies the strikethrough to all the children passed recursively + */ +function applyStrikeThrough(children: React.ReactNode) { + return React.Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child; + } + const props: {style: Array; children?: React.ReactNode} = { + style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted, styles.userSelectNone), + }; + if (child.props.children) { + props.children = applyStrikeThrough(child.props.children); + } + return React.cloneElement(child, props); + }); +} + +function OfflineWithFeedback({ + pendingAction = null, + canDismissError = true, + contentContainerStyle = [], + errorRowStyles = [], + errors = null, + needsOffscreenAlphaCompositing = false, + onClose = () => {}, + shouldDisableOpacity = false, + shouldDisableStrikeThrough = false, + shouldHideOnDelete = true, + shouldShowErrorMessages = true, + style = [], + ...props +}: OfflineWithFeedbackProps) { + const {isOffline} = useNetwork(); + + const hasErrors = !isEmptyObject(errors); + // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. + const errorMessages = lodashOmitBy(errors, (e) => e === null); + const hasErrorMessages = !isEmptyObject(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 = 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); + } + return ( + + {!hideChildren && ( + + {children} + + )} + {shouldShowErrorMessages && hasErrorMessages && ( + + )} + + ); +} + +OfflineWithFeedback.displayName = 'OfflineWithFeedback'; + +export default OfflineWithFeedback; diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts new file mode 100644 index 000000000000..7f72ecb631d2 --- /dev/null +++ b/src/types/utils/EmptyObject.ts @@ -0,0 +1,15 @@ +import Falsy from './Falsy'; + +type EmptyObject = Record; + +// eslint-disable-next-line rulesdir/no-negated-variables +function isNotEmptyObject | Falsy>(arg: T | EmptyObject): arg is T { + return Object.keys(arg ?? {}).length > 0; +} + +function isEmptyObject(obj: T): boolean { + return Object.keys(obj ?? {}).length === 0; +} + +export {isNotEmptyObject, isEmptyObject}; +export type {EmptyObject}; diff --git a/src/types/utils/Falsy.ts b/src/types/utils/Falsy.ts new file mode 100644 index 000000000000..c1bd7527a223 --- /dev/null +++ b/src/types/utils/Falsy.ts @@ -0,0 +1,3 @@ +type Falsy = undefined | null | false; + +export default Falsy; From 1323fae1f74998e283b526877670a26790d1800d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 13 Nov 2023 16:30:02 +0100 Subject: [PATCH 2/8] fix: migrate MessagesRow to TS --- .../{MessagesRow.js => MessagesRow.tsx} | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) rename src/components/{MessagesRow.js => MessagesRow.tsx} (81%) diff --git a/src/components/MessagesRow.js b/src/components/MessagesRow.tsx similarity index 81% rename from src/components/MessagesRow.js rename to src/components/MessagesRow.tsx index e49fca97cef7..c21ecbfcbe8e 100644 --- a/src/components/MessagesRow.js +++ b/src/components/MessagesRow.tsx @@ -1,11 +1,9 @@ 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 styles from '@styles/styles'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; import DotIndicatorMessage from './DotIndicatorMessage'; import Icon from './Icon'; @@ -37,14 +35,22 @@ const defaultProps = { canDismiss: true, }; -function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) { +type MessagesRowProps = { + messages: Record; + type: string; + onClose: () => void; + containerStyles: StyleProp; + canDismiss: boolean; +}; + +function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles = [], canDismiss = true}: MessagesRowProps) { const {translate} = useLocalize(); - if (_.isEmpty(messages)) { + if (Object.keys(messages).length === 0) { return null; } return ( - + Date: Fri, 24 Nov 2023 11:17:27 +0100 Subject: [PATCH 3/8] fix: lint errors --- src/components/OfflineWithFeedback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 3fe7fd99633f..0613cd484601 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -96,7 +96,7 @@ function OfflineWithFeedback({ // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. const errorMessages = lodashOmitBy(errors, (e) => e === null); const hasErrorMessages = !isEmptyObject(errorMessages); - const isOfflinePendingAction = isOffline && pendingAction; + 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); From c0b19a972dbf95fbdeebd907a5e6bb943a4ceeb0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 24 Nov 2023 13:05:38 +0100 Subject: [PATCH 4/8] fix: resolve comments --- src/components/MessagesRow.tsx | 42 ++++++++------------------ src/components/OfflineWithFeedback.tsx | 14 ++++++--- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/src/components/MessagesRow.tsx b/src/components/MessagesRow.tsx index d468f9efae8d..54e76a93b156 100644 --- a/src/components/MessagesRow.tsx +++ b/src/components/MessagesRow.tsx @@ -9,43 +9,27 @@ import * as Expensicons from './Icon/Expensicons'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip'; -// const propTypes = { -// /** The messages to display */ -// messages: PropTypes.objectOf( -// PropTypes.oneOfType([PropTypes.oneOfType([PropTypes.string, PropTypes.object]), PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), -// ), - -// /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ -// type: PropTypes.oneOf(['error', 'success']).isRequired, - -// /** A function to run when the X button next to the message is clicked */ -// onClose: PropTypes.func, +type MessagesRowProps = { + /** The messages to display */ + messages: Record; -// /** Additional style object for the container */ -// containerStyles: stylePropTypes, + /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ + type: 'error' | 'success'; -// /** Whether we can dismiss the messages */ -// canDismiss: PropTypes.bool, -// }; + /** A function to run when the X button next to the message is clicked */ + onClose?: () => void; -// const defaultProps = { -// messages: {}, -// onClose: () => {}, -// containerStyles: [], -// canDismiss: true, -// }; + /** Additional style object for the container */ + containerStyles?: StyleProp; -type MessagesRowProps = { - messages: Record; - type: 'error' | 'success'; - onClose: () => void; - containerStyles: StyleProp; - canDismiss: boolean; + /** Whether we can dismiss the messages */ + canDismiss?: boolean; }; function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles, canDismiss = true}: MessagesRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + if (Object.keys(messages).length === 0) { return null; } @@ -53,7 +37,7 @@ function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles, return ( diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 0613cd484601..40fc0d717dc9 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -57,20 +57,24 @@ type OfflineWithFeedbackProps = { canDismissError?: boolean; }; +type StrikethroughProps = {style: Array; children?: React.ReactNode}; /** * This method applies the strikethrough to all the children passed recursively */ -function applyStrikeThrough(children: React.ReactNode) { +function applyStrikeThrough(children: React.ReactNode): React.ReactNode { return React.Children.map(children, (child) => { if (!React.isValidElement(child)) { return child; } - const props: {style: Array; children?: React.ReactNode} = { + + const props: StrikethroughProps = { style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted, styles.userSelectNone), }; + if (child.props.children) { props.children = applyStrikeThrough(child.props.children); } + return React.cloneElement(child, props); }); } @@ -78,8 +82,8 @@ function applyStrikeThrough(children: React.ReactNode) { function OfflineWithFeedback({ pendingAction = null, canDismissError = true, - contentContainerStyle = [], - errorRowStyles = [], + contentContainerStyle, + errorRowStyles, errors = null, needsOffscreenAlphaCompositing = false, onClose = () => {}, @@ -87,7 +91,7 @@ function OfflineWithFeedback({ shouldDisableStrikeThrough = false, shouldHideOnDelete = true, shouldShowErrorMessages = true, - style = [], + style, ...props }: OfflineWithFeedbackProps) { const {isOffline} = useNetwork(); From 03126d49726fe0159a7101a4b0e78481ea9325bd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 27 Nov 2023 10:07:49 +0100 Subject: [PATCH 5/8] fix: resolve comments --- src/components/OfflineWithFeedback.tsx | 4 +++- src/types/utils/EmptyObject.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 40fc0d717dc9..008ba1bd74e2 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -7,6 +7,7 @@ import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MessagesRow from './MessagesRow'; @@ -57,7 +58,8 @@ type OfflineWithFeedbackProps = { canDismissError?: boolean; }; -type StrikethroughProps = {style: Array; children?: React.ReactNode}; +type StrikethroughProps = Partial & {style: Array}; + /** * This method applies the strikethrough to all the children passed recursively */ diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts index 7f72ecb631d2..50a37b5edee5 100644 --- a/src/types/utils/EmptyObject.ts +++ b/src/types/utils/EmptyObject.ts @@ -3,7 +3,7 @@ import Falsy from './Falsy'; type EmptyObject = Record; // eslint-disable-next-line rulesdir/no-negated-variables -function isNotEmptyObject | Falsy>(arg: T | EmptyObject): arg is T { +function isNotEmptyObject | Falsy>(arg: T): arg is T { return Object.keys(arg ?? {}).length > 0; } From 53cd80ed148e003aca822f621df4a91c87c3de14 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 27 Nov 2023 10:08:30 +0100 Subject: [PATCH 6/8] fix: resolve comment --- src/types/utils/EmptyObject.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts index 50a37b5edee5..3bb82ba4b2c8 100644 --- a/src/types/utils/EmptyObject.ts +++ b/src/types/utils/EmptyObject.ts @@ -7,7 +7,7 @@ function isNotEmptyObject | Falsy>(arg: T): ar return Object.keys(arg ?? {}).length > 0; } -function isEmptyObject(obj: T): boolean { +function isEmptyObject | Falsy>(obj: T): boolean { return Object.keys(obj ?? {}).length === 0; } From b88897be24d65561129510d8bc86872535f71a9c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 27 Nov 2023 10:20:05 +0100 Subject: [PATCH 7/8] fix: resolve comment --- src/components/OfflineWithFeedback.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 008ba1bd74e2..e2808843dc3d 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -17,7 +17,7 @@ import MessagesRow from './MessagesRow'; * care of adding the appropriate styles for pending actions and displaying the dismissible error. */ -type OfflineWithFeedbackProps = { +type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ pendingAction: OnyxCommon.PendingAction | null; @@ -36,9 +36,6 @@ type OfflineWithFeedbackProps = { /** A function to run when the X button next to the error is clicked */ onClose?: () => void; - /** The content that needs offline feedback */ - children: React.ReactNode; - /** Additional styles to add after local styles. Applied to the parent container */ style?: StyleProp; From 26782f99409423cb4e38bfbf2d0b006095697b1b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 30 Nov 2023 13:19:21 +0100 Subject: [PATCH 8/8] fix: resolve comments --- src/components/MessagesRow.tsx | 6 ++++-- src/components/OfflineWithFeedback.tsx | 28 +++++++++++++++----------- src/types/utils/EmptyObject.ts | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/MessagesRow.tsx b/src/components/MessagesRow.tsx index 54e76a93b156..02b78942dfcf 100644 --- a/src/components/MessagesRow.tsx +++ b/src/components/MessagesRow.tsx @@ -1,8 +1,10 @@ import React from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; +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'; @@ -11,7 +13,7 @@ import Tooltip from './Tooltip'; type MessagesRowProps = { /** The messages to display */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; @@ -30,7 +32,7 @@ function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles, const styles = useThemeStyles(); const {translate} = useLocalize(); - if (Object.keys(messages).length === 0) { + if (isEmptyObject(messages)) { return null; } diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 999031a3fca5..17772e148f53 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -1,4 +1,3 @@ -import lodashOmitBy from 'lodash/omitBy'; import React from 'react'; import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; @@ -8,7 +7,7 @@ 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 {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; import MessagesRow from './MessagesRow'; /** @@ -19,13 +18,13 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction: OnyxCommon.PendingAction | null; + pendingAction: OnyxCommon.PendingAction; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; /** The errors to display */ - errors?: OnyxCommon.Errors | null; + errors?: OnyxCommon.Errors; /** Whether we should show the error messages */ shouldShowErrorMessages?: boolean; @@ -60,7 +59,7 @@ type StrikethroughProps = Partial & {style: Array): React.ReactNode { return React.Children.map(children, (child) => { if (!React.isValidElement(child)) { return child; @@ -78,12 +77,17 @@ function applyStrikeThrough(children: React.ReactNode, styles: any): React.React }); } +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 = null, + pendingAction, canDismissError = true, contentContainerStyle, errorRowStyles, - errors = null, + errors, needsOffscreenAlphaCompositing = false, onClose = () => {}, shouldDisableOpacity = false, @@ -91,22 +95,22 @@ function OfflineWithFeedback({ shouldHideOnDelete = true, shouldShowErrorMessages = true, style, - ...props + ...rest }: OfflineWithFeedbackProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); - const hasErrors = !isEmptyObject(errors); + 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 = lodashOmitBy(errors, (e) => e === null); - const hasErrorMessages = !isEmptyObject(errorMessages); + 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 = props.children; + let children = rest.children; // Apply strikethrough to children if needed, but skip it if we are not going to render them if (needsStrikeThrough && !hideChildren) { diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts index ee11c726ea2a..9b9f3244a5f8 100644 --- a/src/types/utils/EmptyObject.ts +++ b/src/types/utils/EmptyObject.ts @@ -7,7 +7,7 @@ function isNotEmptyObject | Falsy>(arg: T | Em return Object.keys(arg ?? {}).length > 0; } -function isEmptyObject | Falsy>(obj: T): boolean { +function isEmptyObject(obj: T): boolean { return Object.keys(obj ?? {}).length === 0; }