diff --git a/src/ROUTES.js b/src/ROUTES.js index ab583dd6935f4..31c3badc0a5b9 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -80,6 +80,8 @@ export default { getWorkspaceInviteRoute: policyID => `workspace/${policyID}/invite`, WORKSPACE_INVITE: 'workspace/:policyID/invite', REQUEST_CALL: 'request-call', + getWorkspaceEditorRoute: policyID => `workspace/${policyID}/edit`, + WORKSPACE_EDITOR: 'workspace/:policyID/edit', VALIDATE_CODE_URL: (accountID, validateCode, exitTo = '') => { const exitToURL = exitTo ? `?exitTo=${exitTo}` : ''; return `v/${accountID}/${validateCode}${exitToURL}`; diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 219203bb57f87..27f969259bd24 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -12,6 +12,7 @@ import styles from '../styles/styles'; import themeColors from '../styles/themes/default'; import AttachmentPicker from './AttachmentPicker'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import variables from '../styles/variables'; const propTypes = { /** Avatar URL to display */ @@ -114,10 +115,15 @@ class AvatarWithImagePicker extends React.Component { {({openPicker}) => ( <> this.setState({isMenuVisible: true})} > - + `You have been invited to the ${workspaceName} Workspace! Download the Expensify mobile App to start tracking your expenses.`, }, + editor: { + title: 'Edit Workspace', + nameInputLabel: 'Name', + nameInputHelpText: 'This is the name you will see on your Workspace.', + save: 'Save', + genericFailureMessage: 'An error occurred updating the workspace, please try again.', + avatarUploadFailureMessage: 'An error occurred uploading the avatar, please try again.', + }, }, requestCallPage: { requestACall: 'Request a Call', diff --git a/src/languages/es.js b/src/languages/es.js index c8b487901248a..a09d85524cf58 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -584,6 +584,14 @@ export default { genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', welcomeNote: ({workspaceName}) => `¡Has sido invitado a la ${workspaceName} Espacio de trabajo! Descargue la aplicación móvil Expensify para comenzar a rastrear sus gastos.`, }, + editor: { + title: 'Editar espacio de trabajo', + nameInputLabel: 'Nombre', + nameInputHelpText: 'Este es el nombre que verás en tu espacio de trabajo.', + save: 'Guardar', + genericFailureMessage: 'Se produjo un error al guardar el espacio de trabajo. Por favor, inténtalo de nuevo.', + avatarUploadFailureMessage: 'No se pudo subir el avatar. Por favor, inténtalo de nuevo.', + }, }, requestCallPage: { requestACall: 'Llámame por teléfono', diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 028ff82ea4438..065306faa4c01 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -58,6 +58,7 @@ import { WorkspaceInviteModalStackNavigator, RequestCallModalStackNavigator, ReportDetailsModalStackNavigator, + WorkspaceEditorNavigator, } from './ModalStackNavigators'; import SCREENS from '../../../SCREENS'; import Timers from '../../Timers'; @@ -408,6 +409,12 @@ class AuthScreens extends React.Component { component={IOUSendModalStackNavigator} listeners={modalScreenListeners} /> + ); } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 326dc6ed11ad0..786228868dc19 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -28,6 +28,7 @@ import ReimbursementAccountPage from '../../../pages/ReimbursementAccount/Reimbu import NewWorkspacePage from '../../../pages/workspace/NewWorkspacePage'; import RequestCallPage from '../../../pages/RequestCallPage'; import ReportDetailsPage from '../../../pages/ReportDetailsPage'; +import WorkspaceEditorPage from '../../../pages/workspace/WorkspaceEditorPage'; const defaultSubRouteOptions = { cardStyle: styles.navigationScreenCardStyle, @@ -197,6 +198,11 @@ const RequestCallModalStackNavigator = createModalStackNavigator([{ name: 'RequestCall_Root', }]); +const WorkspaceEditorNavigator = createModalStackNavigator([{ + Component: WorkspaceEditorPage, + name: 'WorkspaceEditor_Root', +}]); + export { IOUBillStackNavigator, IOURequestModalStackNavigator, @@ -215,4 +221,5 @@ export { NewWorkspaceStackNavigator, WorkspaceInviteModalStackNavigator, RequestCallModalStackNavigator, + WorkspaceEditorNavigator, }; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index cf3c57b07ac6b..48ace4ba68ae2 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -157,6 +157,12 @@ export default { }, }, + WorkspaceEditor: { + screens: { + WorkspaceEditor_Root: ROUTES.WORKSPACE_EDITOR, + }, + }, + RequestCall: { screens: { RequestCall_Root: ROUTES.REQUEST_CALL, diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 4965508c8b166..251b2470b3698 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -207,35 +207,41 @@ function create(name) { } /** - * Sets avatar or removes it if called with no avatarURL - * - * @param {String} policyID - * @param {String} [avatarURL] + * @param {Object} file + * @returns {Promise} */ -function setAvatarURL(policyID, avatarURL = '') { - API.UpdatePolicy({policyID, value: JSON.stringify({avatarURL}), lastModified: null}) - .then((policyResponse) => { - if (policyResponse.jsonCode !== 200) { +function uploadAvatar(file) { + return API.User_UploadAvatar({file}) + .then((response) => { + if (response.jsonCode !== 200) { + // Show the user feedback + const errorMessage = translateLocal('workspace.editor.avatarUploadFailureMessage'); + Growl.error(errorMessage, 5000); return; } - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {avatarURL}); + return response.s3url; }); } /** + * Sets the name of the policy + * * @param {String} policyID - * @param {Object} file + * @param {Object} values */ -function updateAvatar(policyID, file) { - API.User_UploadAvatar({file}) - .then((response) => { - if (response.jsonCode !== 200) { +function update(policyID, values) { + API.UpdatePolicy({policyID, value: JSON.stringify(values), lastModified: null}) + .then((policyResponse) => { + if (policyResponse.jsonCode !== 200) { + // Show the user feedback + const errorMessage = translateLocal('workspace.editor.genericFailureMessage'); + Growl.error(errorMessage, 5000); return; } - // Once we get the s3url back, update the policy with the new avatar URL - setAvatarURL(policyID, response.s3url); + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, values); + Navigation.dismissModal(); }); } @@ -245,6 +251,6 @@ export { removeMembers, invite, create, - updateAvatar, - setAvatarURL, + uploadAvatar, + update, }; diff --git a/src/pages/settings/InitialPage.js b/src/pages/settings/InitialPage.js index 064691876484a..9305ba6a6e63b 100755 --- a/src/pages/settings/InitialPage.js +++ b/src/pages/settings/InitialPage.js @@ -1,5 +1,5 @@ import React from 'react'; -import {View, ScrollView} from 'react-native'; +import {View, ScrollView, Pressable} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; @@ -153,6 +153,9 @@ const InitialSettingsPage = ({ .value(); menuItems.push(...defaultMenuItems); + + const openProfileSettings = () => Navigation.navigate(ROUTES.SETTINGS_PROFILE); + return ( - + - - - {myPersonalDetails.displayName - ? myPersonalDetails.displayName - : Str.removeSMSDomain(session.email)} - + + + + + {myPersonalDetails.displayName + ? myPersonalDetails.displayName + : Str.removeSMSDomain(session.email)} + + {myPersonalDetails.displayName && ( new Promise((resolve) => { + this.setState({avatarURL: url}, resolve); + })); + } + + onImageRemoved() { + this.setState({previewAvatarURL: '', avatarURL: ''}); + } + + submit() { + // Wait for the upload avatar promise to finish before updating the policy + this.uploadAvatarPromise.then(() => { + const name = this.state.name.trim(); + const avatarURL = this.state.avatarURL; + const policyID = this.props.policy.id; + + update(policyID, {name, avatarURL}); + }); + } + + render() { + const {policy} = this.props; + + if (!Permissions.canUseFreePlan(this.props.betas)) { + console.debug('Not showing workspace editor page because user is not on free plan beta'); + return ; + } + + if (_.isEmpty(policy)) { + return null; + } + + return ( + + + + + + ( + + )} + style={[styles.mb3]} + anchorPosition={{top: 172, right: 18}} + isUsingDefaultAvatar={!this.state.previewAvatarURL} + onImageSelected={this.onImageSelected} + onImageRemoved={this.onImageRemoved} + /> + + this.setState({name})} + onSubmitEditting={this.submit} + /> + + {this.props.translate('workspace.editor.nameInputHelpText')} + + + +