From b7fb045e6274c4cf1f3817e471c2f7acb5a49ba4 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Fri, 8 Oct 2021 15:06:06 +0530 Subject: [PATCH 01/14] refactor Invite Member page --- src/pages/workspace/WorkspaceInvitePage.js | 279 ++++++++++++++------- 1 file changed, 184 insertions(+), 95 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 2ad3862a36c4..f35616fc3242 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -1,8 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - View, ScrollView, Pressable, Linking, -} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import _ from 'underscore'; @@ -12,22 +10,35 @@ import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import styles from '../../styles/styles'; -import Text from '../../components/Text'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import {hideWorkspaceAlertMessage, invite, setWorkspaceErrors} from '../../libs/actions/Policy'; import ExpensiTextInput from '../../components/ExpensiTextInput'; import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; import {isSystemUser} from '../../libs/userUtils'; -import {addSMSDomainIfPhoneNumber} from '../../libs/OptionsListUtils'; -import Icon from '../../components/Icon'; -import {NewWindow} from '../../components/Icon/Expensicons'; -import variables from '../../styles/variables'; -import CONST from '../../CONST'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; +import OptionsSelector from '../../components/OptionsSelector'; +import {getNewGroupOptions, getHeaderMessage, addSMSDomainIfPhoneNumber} from '../../libs/OptionsListUtils'; +import {EXCLUDED_GROUP_EMAILS} from '../../CONST'; + +const personalDetailsPropTypes = PropTypes.shape({ + /** The login of the person (either email or phone number) */ + login: PropTypes.string.isRequired, + + /** The URL of the person's avatar (there should already be a default avatar if + the person doesn't have their own avatar uploaded yet) */ + avatar: PropTypes.string.isRequired, + + /** This is either the user's full name, or their login if full name is an empty string */ + displayName: PropTypes.string.isRequired, +}); const propTypes = { - ...withLocalizePropTypes, + /** Beta features list */ + betas: PropTypes.arrayOf(PropTypes.string).isRequired, + + /** All of the personal details for everyone */ + personalDetails: PropTypes.objectOf(personalDetailsPropTypes).isRequired, /** The policy passed via the route */ policy: PropTypes.shape({ @@ -43,6 +54,9 @@ const propTypes = { policyID: PropTypes.string, }), }).isRequired, + + ...withLocalizePropTypes, + }; const defaultProps = { @@ -55,22 +69,40 @@ class WorkspaceInvitePage extends React.Component { constructor(props) { super(props); + this.inviteUser = this.inviteUser.bind(this); + this.clearErrors = this.clearErrors.bind(this); + this.getExcludedUsers = this.getExcludedUsers.bind(this); + this.toggleOption = this.toggleOption.bind(this); + const { + personalDetails, + userToInvite, + } = getNewGroupOptions( + [], + props.personalDetails, + props.betas, + '', + [], + this.getExcludedUsers(), + ); this.state = { - userLogins: '', + searchValue: '', + personalDetails, + selectedOptions: [], + userToInvite, welcomeNote: this.getWelcomeNotePlaceholder(), foundSystemLogin: '', }; - - this.focusEmailOrPhoneInput = this.focusEmailOrPhoneInput.bind(this); - this.inviteUser = this.inviteUser.bind(this); - this.clearErrors = this.clearErrors.bind(this); - this.emailOrPhoneInputRef = null; } componentDidMount() { this.clearErrors(); } + getExcludedUsers() { + const policyEmployeeList = lodashGet(this.props, 'policy.employeeList', []); + return [...EXCLUDED_GROUP_EMAILS, policyEmployeeList]; + } + /** * Gets the welcome note default text * @@ -110,16 +142,88 @@ class WorkspaceInvitePage extends React.Component { return _.size(lodashGet(this.props.policy, 'errors', {})) > 0 || this.props.policy.alertMessage.length > 0; } + /** + * Returns the sections needed for the OptionsSelector + * + * @param {Boolean} maxParticipantsReached + * @returns {Array} + */ + getSections(maxParticipantsReached) { + const sections = []; + sections.push({ + title: undefined, + data: this.state.selectedOptions, + shouldShow: true, + indexOffset: 0, + }); + + if (maxParticipantsReached) { + return sections; + } + + sections.push({ + title: this.props.translate('common.contacts'), + data: this.state.personalDetails, + shouldShow: !_.isEmpty(this.state.personalDetails), + indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0), + }); + + if (this.state.userToInvite) { + sections.push(({ + undefined, + data: [this.state.userToInvite], + shouldShow: true, + indexOffset: 0, + })); + } + + return sections; + } + clearErrors() { setWorkspaceErrors(this.props.route.params.policyID, {}); hideWorkspaceAlertMessage(this.props.route.params.policyID); } - focusEmailOrPhoneInput() { - if (!this.emailOrPhoneInputRef) { - return; - } - this.emailOrPhoneInputRef.focus(); + /** + * Removes a selected option from list if already selected. If not already selected add this option to the list. + * @param {Object} option + */ + toggleOption(option) { + this.setState((prevState) => { + const isOptionInList = _.some(prevState.selectedOptions, selectedOption => ( + selectedOption.login === option.login + )); + + let newSelectedOptions; + + if (isOptionInList) { + newSelectedOptions = _.reject(prevState.selectedOptions, selectedOption => ( + selectedOption.login === option.login + )); + } else { + newSelectedOptions = [...prevState.selectedOptions, option]; + } + + const { + personalDetails, + userToInvite, + } = getNewGroupOptions( + [], + this.props.personalDetails, + this.props.betas, + isOptionInList ? prevState.searchValue : '', + newSelectedOptions, + this.getExcludedUsers(), + ); + + return { + selectedOptions: newSelectedOptions, + personalDetails, + userToInvite, + searchValue: isOptionInList ? prevState.searchValue : '', + }; + }); } /** @@ -163,6 +267,12 @@ class WorkspaceInvitePage extends React.Component { } render() { + const sections = this.getSections(); + const headerMessage = getHeaderMessage( + this.state.personalDetails.length !== 0, + Boolean(this.state.userToInvite), + this.state.searchValue, + ); return ( @@ -173,79 +283,52 @@ class WorkspaceInvitePage extends React.Component { Navigation.dismissModal(); }} /> - this.form = el} - contentContainerStyle={styles.flexGrow1} - keyboardShouldPersistTaps="handled" - > - {/* Form elements */} - - - {this.props.translate('workspace.invite.invitePeoplePrompt')} - - - this.emailOrPhoneInputRef = el} - label={this.props.translate('workspace.invite.enterEmailOrPhone')} - placeholder={this.props.translate('workspace.invite.EmailOrPhonePlaceholder')} - autoCompleteType="email" - autoCorrect={false} - autoCapitalize="none" - multiline - numberOfLines={2} - value={this.state.userLogins} - onChangeText={(text) => { - this.clearErrors(); - this.setState({userLogins: text, foundSystemLogin: ''}); - }} - errorText={this.getErrorText()} - /> - - - this.setState({welcomeNote: text})} - /> - - { - e.preventDefault(); - Linking.openURL(CONST.PRIVACY_URL); - }} - accessibilityRole="link" - href={CONST.PRIVACY_URL} - > - {({hovered, pressed}) => ( - - - {this.props.translate('common.privacyPolicy')} - - - - - - )} - - - + + { + const { + personalDetails, + userToInvite, + } = getNewGroupOptions( + [], + this.props.personalDetails, + this.props.betas, + searchValue, + [], + this.getExcludedUsers(), + ); + this.setState({ + searchValue, + userToInvite, + personalDetails, + }); + }} + headerMessage={headerMessage} + disableArrowKeysActions + hideSectionHeaders + hideAdditionalOptionStates + forceTextUnreadStyle + shouldFocusOnSelectRow + /> + + + + this.setState({welcomeNote: text})} + /> - + ); @@ -269,8 +352,14 @@ WorkspaceInvitePage.defaultProps = defaultProps; export default compose( withLocalize, withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS, + }, policy: { key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, }, + betas: { + key: ONYXKEYS.BETAS, + }, }), )(WorkspaceInvitePage); From 1d462381ce3b67dae39517795eb7d0b81cb21bd0 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Fri, 8 Oct 2021 15:08:43 +0530 Subject: [PATCH 02/14] exclude existing members --- src/pages/workspace/WorkspaceInvitePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index f35616fc3242..94121757748e 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -100,7 +100,7 @@ class WorkspaceInvitePage extends React.Component { getExcludedUsers() { const policyEmployeeList = lodashGet(this.props, 'policy.employeeList', []); - return [...EXCLUDED_GROUP_EMAILS, policyEmployeeList]; + return [...EXCLUDED_GROUP_EMAILS, ...policyEmployeeList]; } /** From 4a8161cf5195820c38929558321ca49eb63d9fb4 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Fri, 8 Oct 2021 20:04:18 +0530 Subject: [PATCH 03/14] adjustments --- src/languages/en.js | 6 +-- src/languages/es.js | 6 +-- src/pages/workspace/WorkspaceInvitePage.js | 59 +++++----------------- 3 files changed, 15 insertions(+), 56 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 7f7ecf8c5217..cd805f3f7cdc 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -650,12 +650,8 @@ export default { invitePeople: 'Invite new members', invitePeoplePrompt: 'Invite new members to your workspace.', personalMessagePrompt: 'Add a personal message (optional)', - enterEmailOrPhone: 'Emails or phone numbers', - EmailOrPhonePlaceholder: 'Enter comma-separated list of emails or phone numbers', - pleaseEnterValidLogin: 'Please ensure the email or phone number is valid (e.g. +15005550006).', - pleaseEnterUniqueLogin: 'That user is already a member of this workspace.', + pleaseSelectUser: 'Please select a user from contacts.', genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', - systemUserError: ({email}) => `Sorry, you cannot invite ${email} to a workspace.`, welcomeNote: ({workspaceName}) => `You have been invited to ${workspaceName}! Download the Expensify mobile app to start tracking your expenses.`, }, editor: { diff --git a/src/languages/es.js b/src/languages/es.js index fa9b594af0ee..a6a2603ceef4 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -652,12 +652,8 @@ export default { invitePeople: 'Invitar nuevos miembros', invitePeoplePrompt: 'Invita nuevos miembros a tu espacio de trabajo.', personalMessagePrompt: 'Agregar un mensaje personal (Opcional)', - enterEmailOrPhone: 'Correos electrónicos o números de teléfono', - EmailOrPhonePlaceholder: 'Introduce una lista de correos electrónicos o números de teléfono separado por comas', - pleaseEnterValidLogin: 'Asegúrese de que el correo electrónico o el número de teléfono sean válidos (e.g. +15005550006).', - pleaseEnterUniqueLogin: 'Ese usuario ya es miembro de este espacio de trabajo.', + pleaseSelectUser: 'Asegúrese de que el correo electrónico o el número de teléfono sean válidos (e.g. +15005550006).', genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', - systemUserError: ({email}) => `Lo sentimos, no puedes invitar a ${email} a un espacio de trabajo.`, welcomeNote: ({workspaceName}) => `¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify para comenzar a rastrear sus gastos.`, }, editor: { diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 94121757748e..4523b12eb105 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import Str from 'expensify-common/lib/str'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; @@ -15,10 +14,9 @@ import ONYXKEYS from '../../ONYXKEYS'; import {hideWorkspaceAlertMessage, invite, setWorkspaceErrors} from '../../libs/actions/Policy'; import ExpensiTextInput from '../../components/ExpensiTextInput'; import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; -import {isSystemUser} from '../../libs/userUtils'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import OptionsSelector from '../../components/OptionsSelector'; -import {getNewGroupOptions, getHeaderMessage, addSMSDomainIfPhoneNumber} from '../../libs/OptionsListUtils'; +import {getNewGroupOptions, getHeaderMessage} from '../../libs/OptionsListUtils'; import {EXCLUDED_GROUP_EMAILS} from '../../CONST'; const personalDetailsPropTypes = PropTypes.shape({ @@ -90,7 +88,6 @@ class WorkspaceInvitePage extends React.Component { selectedOptions: [], userToInvite, welcomeNote: this.getWelcomeNotePlaceholder(), - foundSystemLogin: '', }; } @@ -120,16 +117,8 @@ class WorkspaceInvitePage extends React.Component { getErrorText() { const errors = lodashGet(this.props.policy, 'errors', {}); - if (errors.invalidLogin) { - return this.props.translate('workspace.invite.pleaseEnterValidLogin'); - } - - if (errors.systemUserError) { - return this.props.translate('workspace.invite.systemUserError', {email: this.state.foundSystemLogin}); - } - - if (errors.duplicateLogin) { - return this.props.translate('workspace.invite.pleaseEnterUniqueLogin'); + if (errors.noUserSelected) { + return this.props.translate('workspace.invite.pleaseSelectUser'); } return ''; @@ -144,11 +133,9 @@ class WorkspaceInvitePage extends React.Component { /** * Returns the sections needed for the OptionsSelector - * - * @param {Boolean} maxParticipantsReached * @returns {Array} */ - getSections(maxParticipantsReached) { + getSections() { const sections = []; sections.push({ title: undefined, @@ -157,10 +144,6 @@ class WorkspaceInvitePage extends React.Component { indexOffset: 0, }); - if (maxParticipantsReached) { - return sections; - } - sections.push({ title: this.props.translate('common.contacts'), data: this.state.personalDetails, @@ -190,6 +173,8 @@ class WorkspaceInvitePage extends React.Component { * @param {Object} option */ toggleOption(option) { + this.clearErrors(); + this.setState((prevState) => { const isOptionInList = _.some(prevState.selectedOptions, selectedOption => ( selectedOption.login === option.login @@ -234,35 +219,20 @@ class WorkspaceInvitePage extends React.Component { return; } - const logins = _.map(_.compact(this.state.userLogins.split(',')), login => login.trim()); - invite(logins, this.state.welcomeNote || this.getWelcomeNotePlaceholder(), - this.props.route.params.policyID); + const logins = _.map(this.state.selectedOptions, option => option.login); + invite(logins, this.state.welcomeNote || this.getWelcomeNotePlaceholder(), this.props.route.params.policyID); } /** * @returns {Boolean} */ validate() { - const logins = _.map(_.compact(this.state.userLogins.split(',')), login => login.trim()); - const isEnteredLoginsvalid = _.every(logins, login => Str.isValidEmail(login) || Str.isValidPhone(login)); const errors = {}; - let foundSystemLogin = ''; - if (logins.length <= 0 || !isEnteredLoginsvalid) { - errors.invalidLogin = true; - } - - foundSystemLogin = _.find(logins, login => isSystemUser(login)); - if (foundSystemLogin) { - errors.systemUserError = true; + if (this.state.selectedOptions.length <= 0) { + errors.noUserSelected = true; } - const policyEmployeeList = lodashGet(this.props, 'policy.employeeList', []); - const areLoginsDuplicate = _.some(logins, login => _.contains(policyEmployeeList, addSMSDomainIfPhoneNumber(login))); - if (areLoginsDuplicate) { - errors.duplicateLogin = true; - } - - this.setState({foundSystemLogin}, () => setWorkspaceErrors(this.props.route.params.policyID, errors)); + setWorkspaceErrors(this.props.route.params.policyID, errors); return _.size(errors) <= 0; } @@ -274,7 +244,7 @@ class WorkspaceInvitePage extends React.Component { this.state.searchValue, ); return ( - + { - this.form.scrollTo({y: 0, animated: true}); - }} message={this.props.policy.alertMessage} /> From 5eb901e30711d7463e8efffec252cb26b1a3287d Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Mon, 11 Oct 2021 18:56:30 +0530 Subject: [PATCH 04/14] manage error handling for Workspace invite page --- src/components/FormAlertWithSubmitButton.js | 8 +++++++- src/pages/workspace/WorkspaceInvitePage.js | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js index b85c77486740..d09956edd74a 100644 --- a/src/components/FormAlertWithSubmitButton.js +++ b/src/components/FormAlertWithSubmitButton.js @@ -15,6 +15,9 @@ const propTypes = { /** Whether to show the alert text */ isAlertVisible: PropTypes.bool.isRequired, + /** Whether the button is disabled */ + isDisabled: PropTypes.bool, + /** Submit function */ onSubmit: PropTypes.func.isRequired, @@ -32,10 +35,12 @@ const propTypes = { const defaultProps = { message: '', + isDisabled: false, }; const FormAlertWithSubmitButton = ({ isAlertVisible, + isDisabled, onSubmit, buttonText, translate, @@ -88,9 +93,10 @@ const FormAlertWithSubmitButton = ({ )}