-
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 all 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,131 @@ | ||
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'; | ||
import variables from '../styles/variables'; | ||
|
||
/** | ||
* 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 hasErrors = !_.isEmpty(props.errors); | ||
const isOfflinePendingAction = props.network.isOffline && props.pendingAction; | ||
const isUpdateOrDeleteError = hasErrors && (props.pendingAction === 'delete' || props.pendingAction === 'update'); | ||
const isAddError = hasErrors && props.pendingAction === 'add'; | ||
const needsOpacity = (isOfflinePendingAction && !isUpdateOrDeleteError) || isAddError; | ||
const needsStrikeThrough = props.network.isOffline && props.pendingAction === 'delete'; | ||
const hideChildren = !props.network.isOffline && props.pendingAction === 'delete' && !hasErrors; | ||
let children = props.children; | ||
const sortedErrors = _.chain(props.errors) | ||
.keys() | ||
.sortBy() | ||
.map(key => props.errors[key]) | ||
.value(); | ||
|
||
// 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> | ||
)} | ||
{hasErrors && ( | ||
<View style={styles.offlineFeedback.error}> | ||
<View style={styles.offlineFeedback.errorDot}> | ||
<Icon src={Expensicons.DotIndicator} fill={colors.red} height={variables.iconSizeSmall} width={variables.iconSizeSmall} /> | ||
</View> | ||
<View style={styles.offlineFeedback.textContainer}> | ||
{_.map(sortedErrors, (error, i) => ( | ||
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: because it is hard for me to tell if this has a real impact or not, but it is not recommended by the react doc. It is not recommended to use indexes (
Not sure if it will have a real impact here... but an easy option to it would have been: const sortedErrorKeys = _.chain(props.errors)
.keys()
.sortBy()
// .map(key => props.errors[key]) Don't map this to values!
.value(); then, in your map here you would use the error's key as the 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.
The order of items will never change, so I think we are fine here, no? 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. hmm the index for one of the errors may change if one of the errors is deleted |
||
<Text key={i} 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.