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')}
+
+
+
+
+
+
+ );
+ }
+}
+
+WorkspaceEditorPage.propTypes = propTypes;
+WorkspaceEditorPage.defaultProps = defaultProps;
+
+export default compose(
+ withOnyx({
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ policy: {
+ key: (props) => {
+ const routes = lodashGet(props.navigation.getState(), 'routes', []);
+ const routeWithPolicyIDParam = _.find(routes, route => route.params && route.params.policyID);
+ const policyID = lodashGet(routeWithPolicyIDParam, ['params', 'policyID']);
+ return `${ONYXKEYS.COLLECTION.POLICY}${policyID}`;
+ },
+ },
+ }),
+ withLocalize,
+)(WorkspaceEditorPage);
diff --git a/src/pages/workspace/WorkspaceSidebar.js b/src/pages/workspace/WorkspaceSidebar.js
index e5f3df1609c7a..1086dd5d18804 100644
--- a/src/pages/workspace/WorkspaceSidebar.js
+++ b/src/pages/workspace/WorkspaceSidebar.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
import React from 'react';
-import {View, ScrollView} from 'react-native';
+import {View, ScrollView, Pressable} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
@@ -22,8 +22,7 @@ import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
import compose from '../../libs/compose';
import ONYXKEYS from '../../ONYXKEYS';
-import AvatarWithImagePicker from '../../components/AvatarWithImagePicker';
-import {updateAvatar, setAvatarURL} from '../../libs/actions/Policy';
+import Avatar from '../../components/Avatar';
const propTypes = {
/** Policy for the current route */
@@ -67,6 +66,8 @@ const WorkspaceSidebar = ({translate, isSmallScreenWidth, policy}) => {
return null;
}
+ const openEditor = () => Navigation.navigate(ROUTES.getWorkspaceEditorRoute(policy.id));
+
return (
{
)}
- (
-
- )}
- style={[styles.mb3]}
- anchorPosition={{top: 116, left: 20}}
- isUsingDefaultAvatar={!policy.avatarURL}
- onImageSelected={(image) => {
- updateAvatar(policy.id, image);
- }}
- onImageRemoved={() => setAvatarURL(policy.id)}
- />
-
+ {policy.avatarURL
+ ? (
+
+ )
+ : (
+
+ )}
+
+
+
- {policy.name}
-
+
+ {policy.name}
+
+
{menuItems.map(item => (
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 003646979a59e..146c81e5dd8ed 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1786,16 +1786,18 @@ const styles = {
alignItems: 'center',
backgroundColor: themeColors.icon,
borderColor: themeColors.textReversed,
- borderRadius: 16,
+ borderRadius: 14,
borderWidth: 3,
- bottom: -4,
color: themeColors.textReversed,
- height: 32,
+ height: 28,
+ width: 28,
justifyContent: 'center',
- padding: 4,
+ },
+
+ smallAvatarEditIcon: {
position: 'absolute',
right: -4,
- width: 32,
+ bottom: -4,
},
workspaceCard: {