-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add OfflineWithFeedback component #9931
Changes from 13 commits
915c5a5
bd5500d
706bd19
1374146
d47546c
edcdb20
99370a3
db87b98
9276eb8
6b382be
e7df3c6
740fd19
7daf9cb
045a6da
1bb5c7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,125 @@ | ||||||||||||||
import _ from 'underscore'; | ||||||||||||||
import React from 'react'; | ||||||||||||||
import {Pressable, View} from 'react-native'; | ||||||||||||||
import PropTypes from 'prop-types'; | ||||||||||||||
import compose from '../libs/compose'; | ||||||||||||||
import withLocalize, {withLocalizePropTypes} from './withLocalize'; | ||||||||||||||
import {withNetwork} from './OnyxProvider'; | ||||||||||||||
import networkPropTypes from './networkPropTypes'; | ||||||||||||||
import Text from './Text'; | ||||||||||||||
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 colors from '../styles/colors'; | ||||||||||||||
|
||||||||||||||
/** | ||||||||||||||
* 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']), | ||||||||||||||
|
||||||||||||||
/** The errors to display */ | ||||||||||||||
// eslint-disable-next-line react/forbid-prop-types | ||||||||||||||
errors: PropTypes.object, | ||||||||||||||
|
||||||||||||||
/** A function to run when the X button next to the error is clicked */ | ||||||||||||||
onClose: PropTypes.func.isRequired, | ||||||||||||||
|
||||||||||||||
/** 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 Pressable portion of button */ | ||||||||||||||
style: PropTypes.oneOfType([ | ||||||||||||||
PropTypes.arrayOf(PropTypes.object), | ||||||||||||||
PropTypes.object, | ||||||||||||||
]), | ||||||||||||||
...withLocalizePropTypes, | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
const defaultProps = { | ||||||||||||||
pendingAction: null, | ||||||||||||||
errors: null, | ||||||||||||||
style: [], | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
/** | ||||||||||||||
* This method applies the strikethrough to all the children passed recursively | ||||||||||||||
* @param {Array} children | ||||||||||||||
* @return {Array} | ||||||||||||||
*/ | ||||||||||||||
function applyStrikeThrough(children) { | ||||||||||||||
This comment was marked as resolved.
Sorry, something went wrong. |
||||||||||||||
return React.Children.map(children, (child) => { | ||||||||||||||
if (!React.isValidElement(child)) { | ||||||||||||||
return child; | ||||||||||||||
} | ||||||||||||||
const props = {style: StyleUtils.combineStyles(child.props.style, styles.offlineFeedback.deleted)}; | ||||||||||||||
if (child.props.children) { | ||||||||||||||
props.children = applyStrikeThrough(child.props.children); | ||||||||||||||
} | ||||||||||||||
return React.cloneElement(child, props); | ||||||||||||||
}); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const OfflineWithFeedback = (props) => { | ||||||||||||||
const isOfflinePendingAction = props.network.isOffline && props.pendingAction; | ||||||||||||||
const isUpdateOrDeleteError = props.errors && (props.pendingAction === 'delete' || props.pendingAction === 'update'); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: I think you are considering There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe this should be left as it is and we should not expect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good question... I think you are right and I should be using _.isEmpty. Changing it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To clarify, when we have fieldErrors (or errorFields don't recall what we settled in), then it's totally possible that this would end up being an empty object (since onyx can't really clear things) |
||||||||||||||
const isAddError = props.errors && props.pendingAction === 'add'; | ||||||||||||||
const needsOpacity = (isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError; | ||||||||||||||
const needsStrikeThrough = props.network.isOffline && props.pendingAction === 'delete'; | ||||||||||||||
const hideChildren = !props.network.isOffline && props.pendingAction === 'delete' && !props.errors; | ||||||||||||||
let children = props.children; | ||||||||||||||
const sortedErrors = _.map(_.sortBy(_.keys(props.errors)), key => props.errors[key]); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This may be more readable |
||||||||||||||
|
||||||||||||||
// 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 : {}}> | ||||||||||||||
{children} | ||||||||||||||
</View> | ||||||||||||||
)} | ||||||||||||||
{props.errors && ( | ||||||||||||||
<View style={styles.offlineFeedback.error}> | ||||||||||||||
<View style={styles.offlineFeedback.errorDot}> | ||||||||||||||
<Icon src={Expensicons.DotIndicator} fill={colors.red} height={16} width={16} /> | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: replace 16 by variables.iconSizeSmall (import variables from '../styles/variables';) |
||||||||||||||
</View> | ||||||||||||||
<View style={{flexDirection: 'column', alignItems: 'center', flex: 1}}> | ||||||||||||||
{_.map(sortedErrors, error => ( | ||||||||||||||
<Text style={styles.offlineFeedback.text}>{error}</Text> | ||||||||||||||
))} | ||||||||||||||
</View> | ||||||||||||||
<Tooltip text={props.translate('common.close')}> | ||||||||||||||
<Pressable | ||||||||||||||
onPress={props.onClose} | ||||||||||||||
style={[styles.touchableButtonImage, styles.mr0]} | ||||||||||||||
accessibilityRole="button" | ||||||||||||||
accessibilityLabel={props.translate('common.close')} | ||||||||||||||
> | ||||||||||||||
<Icon src={Expensicons.Close} /> | ||||||||||||||
</Pressable> | ||||||||||||||
</Tooltip> | ||||||||||||||
</View> | ||||||||||||||
)} | ||||||||||||||
</View> | ||||||||||||||
); | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
OfflineWithFeedback.propTypes = propTypes; | ||||||||||||||
OfflineWithFeedback.defaultProps = defaultProps; | ||||||||||||||
|
||||||||||||||
export default compose( | ||||||||||||||
withLocalize, | ||||||||||||||
withNetwork(), | ||||||||||||||
)(OfflineWithFeedback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when I don't want anything to happen when I click the close button, do I need to strictly pass a method that does nothing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would think that you always want to do something with the X button. It would be weird to have an X button that does nothing :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
heh yes, I think you will always have to pass one, though now I realize that maybe we should have this component remove the error box by default and in that case maybe there are actions that do not require an onClose.... though not sure, I will leave this as required, we can remove the required later when we find a case that does not need it.