diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index aa37769156e3..765ca641a3f4 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -1,126 +1,66 @@ -import React, {PureComponent} from 'react'; -import { - View, StyleSheet, Animated, -} from 'react-native'; +import _ from 'underscore'; +import React from 'react'; +import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; import Avatar from './Avatar'; -import themeColors from '../styles/themes/default'; import styles from '../styles/styles'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import SpinningIndicatorAnimation from '../styles/animation/SpinningIndicatorAnimation'; import Tooltip from './Tooltip'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import ONYXKEYS from '../ONYXKEYS'; +import policyMemberPropType from '../pages/policyMemberPropType'; +import * as Policy from '../libs/actions/Policy'; const propTypes = { - /** Is user active? */ - isActive: PropTypes.bool, - /** URL for the avatar */ source: PropTypes.string.isRequired, /** Avatar size */ size: PropTypes.string, - // Whether we show the sync indicator - isSyncing: PropTypes.bool, - /** To show a tooltip on hover */ tooltipText: PropTypes.string, - ...withLocalizePropTypes, + /** The employee list of all policies (coming from Onyx) */ + policiesMemberList: PropTypes.objectOf(policyMemberPropType), }; const defaultProps = { - isActive: false, size: 'default', - isSyncing: false, tooltipText: '', + policiesMemberList: {}, }; -class AvatarWithIndicator extends PureComponent { - constructor(props) { - super(props); - - this.animation = new SpinningIndicatorAnimation(); - } - - componentDidMount() { - if (!this.props.isSyncing) { - return; - } - - this.animation.start(); - } - - componentDidUpdate(prevProps) { - if (!prevProps.isSyncing && this.props.isSyncing) { - this.animation.start(); - } else if (prevProps.isSyncing && !this.props.isSyncing) { - this.animation.stop(); - } - } - - componentWillUnmount() { - this.animation.stop(); - } - - /** - * Returns user status as text - * - * @returns {String} - */ - userStatus() { - if (this.props.isSyncing) { - return this.props.translate('profilePage.syncing'); - } - - if (this.props.isActive) { - return this.props.translate('profilePage.online'); - } - - if (!this.props.isActive) { - return this.props.translate('profilePage.offline'); - } - } - - render() { - const indicatorStyles = [ - styles.alignItemsCenter, - styles.justifyContentCenter, - this.props.size === 'large' ? styles.statusIndicatorLarge : styles.statusIndicator, - this.props.isActive ? styles.statusIndicatorOnline : styles.statusIndicatorOffline, - this.animation.getSyncingStyles(), - ]; - - return ( - - - - - - - {this.props.isSyncing && ( - - )} - - - - ); - } -} +const AvatarWithIndicator = (props) => { + const isLarge = props.size === 'large'; + const indicatorStyles = [ + styles.alignItemsCenter, + styles.justifyContentCenter, + isLarge ? styles.statusIndicatorLarge : styles.statusIndicator, + ]; + + const hasPolicyMemberError = _.some(props.policiesMemberList, policyMembers => Policy.hasPolicyMemberError(policyMembers)); + return ( + + + + {hasPolicyMemberError && ( + + )} + + + ); +}; AvatarWithIndicator.defaultProps = defaultProps; AvatarWithIndicator.propTypes = propTypes; -export default withLocalize(AvatarWithIndicator); +AvatarWithIndicator.displayName = 'AvatarWithIndicator'; + +export default withOnyx({ + policiesMemberList: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST, + }, +})(AvatarWithIndicator); diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js index 196a4da3d403..2422e4745b73 100644 --- a/src/components/OfflineWithFeedback.js +++ b/src/components/OfflineWithFeedback.js @@ -124,6 +124,7 @@ const OfflineWithFeedback = (props) => { OfflineWithFeedback.propTypes = propTypes; OfflineWithFeedback.defaultProps = defaultProps; +OfflineWithFeedback.displayName = 'OfflineWithFeedback'; export default compose( withLocalize, diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 40b96e408565..11fd7f41e493 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -540,7 +540,34 @@ function subscribeToPolicyEvents() { } /** - * Checks if we have any errors stored within the POLICY_MEMBER_LIST. Determines whether we should show a red brick road error or not + * Removes an error after trying to delete a member + * + * @param {String} policyID + * @param {String} memberEmail + */ +function clearDeleteMemberError(policyID, memberEmail) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, { + [memberEmail]: { + pendingAction: null, + errors: null, + }, + }); +} + +/** + * Removes an error after trying to add a member + * + * @param {String} policyID + * @param {String} memberEmail + */ +function clearAddMemberError(policyID, memberEmail) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, { + [memberEmail]: null, + }); +} + +/** +* Checks if we have any errors stored within the POLICY_MEMBER_LIST. Determines whether we should show a red brick road error or not * Data structure: {email: {role:'bla', errors: []}, email2: {role:'bla', errors: [{1231312313: 'Unable to do X'}]}, ...} * @param {Object} policyMemberList * @returns {Boolean} @@ -567,5 +594,7 @@ export { setCustomUnitRate, updateLastAccessedWorkspace, subscribeToPolicyEvents, + clearDeleteMemberError, + clearAddMemberError, hasPolicyMemberError, }; diff --git a/src/pages/policyMemberPropType.js b/src/pages/policyMemberPropType.js index 0e5c39e02369..22a4d355fbfb 100644 --- a/src/pages/policyMemberPropType.js +++ b/src/pages/policyMemberPropType.js @@ -9,4 +9,7 @@ export default PropTypes.shape({ * {: 'error message', : 'error message 2'} */ errors: PropTypes.objectOf(PropTypes.string), + + /** Is this action pending? */ + pendingAction: PropTypes.string, }); diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 7686c5c3b399..35b21c8d8894 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -10,7 +10,8 @@ import themeColors from '../../styles/themes/default'; import Text from '../../components/Text'; import * as Session from '../../libs/actions/Session'; import ONYXKEYS from '../../ONYXKEYS'; -import AvatarWithIndicator from '../../components/AvatarWithIndicator'; +import Tooltip from '../../components/Tooltip'; +import Avatar from '../../components/Avatar'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import * as Expensicons from '../../components/Icon/Expensicons'; @@ -21,8 +22,6 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize import compose from '../../libs/compose'; import CONST from '../../CONST'; import Permissions from '../../libs/Permissions'; -import networkPropTypes from '../../components/networkPropTypes'; -import {withNetwork} from '../../components/OnyxProvider'; import * as App from '../../libs/actions/App'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../components/withCurrentUserPersonalDetails'; import * as Policy from '../../libs/actions/Policy'; @@ -31,9 +30,6 @@ import policyMemberPropType from '../policyMemberPropType'; const propTypes = { /* Onyx Props */ - /** Information about the network */ - network: networkPropTypes.isRequired, - /** The session of the logged in person */ session: PropTypes.shape({ /** Email of the logged in person */ @@ -156,12 +152,13 @@ const InitialSettingsPage = (props) => { - + + + @@ -211,7 +208,6 @@ InitialSettingsPage.displayName = 'InitialSettingsPage'; export default compose( withLocalize, - withNetwork(), withCurrentUserPersonalDetails, withOnyx({ session: { diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 9cb442edb586..eb0c60ab9703 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -1,6 +1,8 @@ import _ from 'underscore'; import React from 'react'; import {View, ScrollView, Pressable} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import styles from '../../styles/styles'; @@ -20,13 +22,21 @@ import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndica import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy'; import * as PolicyActions from '../../libs/actions/Policy'; import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; +import policyMemberPropType from '../policyMemberPropType'; const propTypes = { ...fullPolicyPropTypes, ...withLocalizePropTypes, + + /** The employee list of this policy (coming from Onyx) */ + policyMemberList: PropTypes.objectOf(policyMemberPropType), }; -const defaultProps = fullPolicyDefaultProps; +const defaultProps = { + ...fullPolicyDefaultProps, + policyMemberList: {}, +}; class WorkspaceInitialPage extends React.Component { constructor(props) { @@ -70,6 +80,7 @@ class WorkspaceInitialPage extends React.Component { return ; } + const hasMembersError = PolicyActions.hasPolicyMemberError(this.props.policyMemberList); const menuItems = [ { translationKey: 'workspace.common.settings', @@ -105,6 +116,7 @@ class WorkspaceInitialPage extends React.Component { translationKey: 'workspace.common.members', icon: Expensicons.Users, action: () => Navigation.navigate(ROUTES.getWorkspaceMembersRoute(policy.id)), + error: hasMembersError, }, { translationKey: 'workspace.common.bankAccount', @@ -202,6 +214,7 @@ class WorkspaceInitialPage extends React.Component { iconRight={item.iconRight} onPress={() => item.action()} shouldShowRightIcon + brickRoadIndicator={item.error ? 'error' : null} /> ))} @@ -228,4 +241,9 @@ WorkspaceInitialPage.displayName = 'WorkspaceInitialPage'; export default compose( withLocalize, withFullPolicy, + withOnyx({ + policyMemberList: { + key: ({policy}) => `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policy.id}`, + }, + }), )(WorkspaceInitialPage); diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 764da2999e21..8f10c91b1992 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -27,6 +27,7 @@ import CheckboxWithTooltip from '../../components/CheckboxWithTooltip'; import Hoverable from '../../components/Hoverable'; import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy'; import CONST from '../../CONST'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; const propTypes = { /** The personal details of the person who is logged in */ @@ -186,6 +187,20 @@ class WorkspaceMembersPage extends React.Component { })); } + /** + * Dismisses the errors on one item + * + * @param {Object} item + */ + dismissError(item) { + // TODO: login here also probably will need to change when connecting this to the real api + if (item.pendingAction === 'delete') { + Policy.clearDeleteMemberError(this.props.route.params.policyID, item.login); + } else { + Policy.clearAddMemberError(this.props.route.params.policyID, item.login); + } + } + /** * Do not move this or make it an anonymous function it is a method * so it will not be recreated each time we render an item @@ -203,45 +218,47 @@ class WorkspaceMembersPage extends React.Component { }) { const canBeRemoved = this.props.policy.owner !== item.login && this.props.session.email !== item.login; return ( - this.willTooltipShowForLogin(item.login, true)} onHoverOut={() => this.setState({showTooltipForLogin: ''})}> - this.toggleUser(item.login)} - activeOpacity={0.7} - > - this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}> + this.willTooltipShowForLogin(item.login, true)} onHoverOut={() => this.setState({showTooltipForLogin: ''})}> + this.toggleUser(item.login)} - toggleTooltip={this.state.showTooltipForLogin === item.login} - text={this.props.translate('workspace.people.error.cannotRemove')} - /> - - this.toggleUser(item.login)} - forceTextUnreadStyle - isDisabled={!canBeRemoved} - option={{ - text: Str.removeSMSDomain(item.displayName), - alternateText: Str.removeSMSDomain(item.login), - participantsList: [item], - icons: [item.avatar], - keyForList: item.login, - }} + activeOpacity={0.7} + > + this.toggleUser(item.login)} + toggleTooltip={this.state.showTooltipForLogin === item.login} + text={this.props.translate('workspace.people.error.cannotRemove')} /> - - {this.props.session.email === item.login && ( - - - - {this.props.translate('common.admin')} - - + + this.toggleUser(item.login)} + forceTextUnreadStyle + isDisabled={!canBeRemoved} + option={{ + text: Str.removeSMSDomain(item.displayName), + alternateText: Str.removeSMSDomain(item.login), + participantsList: [item], + icons: [item.avatar], + keyForList: item.login, + }} + /> - )} - - + {this.props.session.email === item.login && ( + + + + {this.props.translate('common.admin')} + + + + )} + + + ); } @@ -252,6 +269,7 @@ class WorkspaceMembersPage extends React.Component { .map(email => this.props.personalDetails[email]) .filter() .sortBy(person => person.displayName.toLowerCase()) + .map(person => ({...person})) // TODO: here we will add the pendingAction and errors prop .value(); const policyID = lodashGet(this.props.route, 'params.policyID'); const policyName = lodashGet(this.props.policy, 'name'); diff --git a/src/styles/styles.js b/src/styles/styles.js index 31efe3cb6be2..6c450b58f0a4 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -982,6 +982,7 @@ const styles = { statusIndicator: { borderColor: themeColors.sidebar, + backgroundColor: themeColors.buttonDangerBG, borderRadius: 6, borderWidth: 2, position: 'absolute', @@ -994,6 +995,7 @@ const styles = { statusIndicatorLarge: { borderColor: themeColors.componentBG, + backgroundColor: themeColors.buttonDangerBG, borderRadius: 8, borderWidth: 2, position: 'absolute', @@ -1008,8 +1010,18 @@ const styles = { backgroundColor: themeColors.online, }, - statusIndicatorOffline: { - backgroundColor: themeColors.offline, + avatarWithIndicator: { + errorDot: { + borderColor: themeColors.sidebar, + borderRadius: 6, + borderWidth: 2, + position: 'absolute', + right: -1, + bottom: -1, + height: 12, + width: 12, + zIndex: 10, + }, }, floatingActionButton: {