-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
OfflineWithFeedback.js
145 lines (126 loc) · 5.96 KB
/
OfflineWithFeedback.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import _ from 'underscore';
import React from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import compose from '../libs/compose';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import {withNetwork} from './OnyxProvider';
import CONST from '../CONST';
import networkPropTypes from './networkPropTypes';
import stylePropTypes from '../styles/stylePropTypes';
import styles from '../styles/styles';
import Tooltip from './Tooltip';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import * as StyleUtils from '../styles/StyleUtils';
import DotIndicatorMessage from './DotIndicatorMessage';
import shouldRenderOffscreen from '../libs/shouldRenderOffscreen';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
/**
* 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,
/** 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,
/** Information about the network */
network: networkPropTypes.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,
...withLocalizePropTypes,
};
const defaultProps = {
pendingAction: null,
shouldHideOnDelete: true,
errors: null,
shouldShowErrorMessages: true,
onClose: () => {},
style: [],
contentContainerStyle: [],
errorRowStyles: [],
};
/**
* 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 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 = props.network.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 = (isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError;
const needsStrikeThrough = props.network.isOffline && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
const hideChildren = props.shouldHideOnDelete && !props.network.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 (
<View style={props.style}>
{!hideChildren && (
<View
style={[needsOpacity ? styles.offlineFeedback.pending : {}, props.contentContainerStyle]}
needsOffscreenAlphaCompositing={shouldRenderOffscreen ? needsOpacity && props.needsOffscreenAlphaCompositing : undefined}
>
{children}
</View>
)}
{props.shouldShowErrorMessages && hasErrorMessages && (
<View style={StyleUtils.combineStyles(styles.offlineFeedback.error, props.errorRowStyles)}>
<DotIndicatorMessage
style={[styles.flex1]}
messages={errorMessages}
type="error"
/>
<Tooltip text={props.translate('common.close')}>
<PressableWithoutFeedback
onPress={props.onClose}
style={[styles.touchableButtonImage]}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={props.translate('common.close')}
>
<Icon src={Expensicons.Close} />
</PressableWithoutFeedback>
</Tooltip>
</View>
)}
</View>
);
}
OfflineWithFeedback.propTypes = propTypes;
OfflineWithFeedback.defaultProps = defaultProps;
OfflineWithFeedback.displayName = 'OfflineWithFeedback';
export default compose(withLocalize, withNetwork())(OfflineWithFeedback);