Skip to content

Commit

Permalink
Merge pull request Expensify#31124 from kubabutkiewicz/ts-migration/O…
Browse files Browse the repository at this point in the history
…fflineWithFeedback/component

[TS migration] Migrate 'OfflineWithFeedback.js' component to TypeScript
  • Loading branch information
cristipaval authored Dec 7, 2023
2 parents 8fa65ec + 26782f9 commit 710ebbf
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 169 deletions.
40 changes: 14 additions & 26 deletions src/components/MessagesRow.js → src/components/MessagesRow.tsx
Original file line number Diff line number Diff line change
@@ -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<string, Localize.MaybePhraseKey>;

/** 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<ViewStyle>;

/** 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 (
<View style={StyleUtils.combineStyles(styles.flexRow, styles.alignItemsCenter, containerStyles)}>
<View style={[styles.flexRow, styles.alignItemsCenter, containerStyles]}>
<DotIndicatorMessage
style={[styles.flex1]}
style={styles.flex1}
messages={messages}
type={type}
/>
Expand All @@ -69,8 +59,6 @@ function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) {
);
}

MessagesRow.propTypes = propTypes;
MessagesRow.defaultProps = defaultProps;
MessagesRow.displayName = 'MessagesRow';

export default MessagesRow;
143 changes: 0 additions & 143 deletions src/components/OfflineWithFeedback.js

This file was deleted.

144 changes: 144 additions & 0 deletions src/components/OfflineWithFeedback.tsx
Original file line number Diff line number Diff line change
@@ -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<ViewStyle>;

/** Additional styles to add after local styles. Applied to the children wrapper container */
contentContainerStyle?: StyleProp<ViewStyle>;

/** Additional style object for the error row */
errorRowStyles?: StyleProp<ViewStyle>;

/** 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<ChildrenProps> & {style: Array<ViewStyle | TextStyle | ImageStyle>};

/**
* This method applies the strikethrough to all the children passed recursively
*/
function applyStrikeThrough(children: React.ReactNode, styles: ReturnType<typeof useThemeStyles>): 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<T>(obj: Record<string, T> | 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 (
<View style={style}>
{!hideChildren && (
<View
style={[needsOpacity ? styles.offlineFeedback.pending : {}, contentContainerStyle]}
needsOffscreenAlphaCompositing={shouldRenderOffscreen ? needsOpacity && needsOffscreenAlphaCompositing : undefined}
>
{children}
</View>
)}
{shouldShowErrorMessages && hasErrorMessages && (
<MessagesRow
messages={errorMessages}
type="error"
onClose={onClose}
containerStyles={errorRowStyles}
canDismiss={canDismissError}
/>
)}
</View>
);
}

OfflineWithFeedback.displayName = 'OfflineWithFeedback';

export default OfflineWithFeedback;

0 comments on commit 710ebbf

Please sign in to comment.