Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
10 changes: 8 additions & 2 deletions src/components/AvatarWithImagePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -114,10 +115,15 @@ class AvatarWithImagePicker extends React.Component {
{({openPicker}) => (
<>
<Pressable
style={[styles.smallEditIcon]}
style={[styles.smallEditIcon, styles.smallAvatarEditIcon]}
onPress={() => this.setState({isMenuVisible: true})}
>
<Icon src={Pencil} fill={themeColors.iconReversed} />
<Icon
src={Pencil}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
fill={themeColors.iconReversed}
/>
</Pressable>
<PopoverMenu
isVisible={this.state.isMenuVisible}
Expand Down
8 changes: 8 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,14 @@ export default {
genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.',
welcomeNote: ({workspaceName}) => `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',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
WorkspaceInviteModalStackNavigator,
RequestCallModalStackNavigator,
ReportDetailsModalStackNavigator,
WorkspaceEditorNavigator,
} from './ModalStackNavigators';
import SCREENS from '../../../SCREENS';
import Timers from '../../Timers';
Expand Down Expand Up @@ -408,6 +409,12 @@ class AuthScreens extends React.Component {
component={IOUSendModalStackNavigator}
listeners={modalScreenListeners}
/>
<RootStack.Screen
name="WorkspaceEditor"
options={modalScreenOptions}
component={WorkspaceEditorNavigator}
listeners={modalScreenListeners}
/>
</RootStack.Navigator>
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -197,6 +198,11 @@ const RequestCallModalStackNavigator = createModalStackNavigator([{
name: 'RequestCall_Root',
}]);

const WorkspaceEditorNavigator = createModalStackNavigator([{
Component: WorkspaceEditorPage,
name: 'WorkspaceEditor_Root',
}]);

export {
IOUBillStackNavigator,
IOURequestModalStackNavigator,
Expand All @@ -215,4 +221,5 @@ export {
NewWorkspaceStackNavigator,
WorkspaceInviteModalStackNavigator,
RequestCallModalStackNavigator,
WorkspaceEditorNavigator,
};
6 changes: 6 additions & 0 deletions src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ export default {
},
},

WorkspaceEditor: {
screens: {
WorkspaceEditor_Root: ROUTES.WORKSPACE_EDITOR,
},
},

RequestCall: {
screens: {
RequestCall_Root: ROUTES.REQUEST_CALL,
Expand Down
42 changes: 24 additions & 18 deletions src/libs/actions/Policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}

Expand All @@ -245,6 +251,6 @@ export {
removeMembers,
invite,
create,
updateAvatar,
setAvatarURL,
uploadAvatar,
update,
};
22 changes: 14 additions & 8 deletions src/pages/settings/InitialPage.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -153,6 +153,9 @@ const InitialSettingsPage = ({
.value();
menuItems.push(...defaultMenuItems);


const openProfileSettings = () => Navigation.navigate(ROUTES.SETTINGS_PROFILE);

return (
<ScreenWrapper>
<HeaderWithCloseButton
Expand All @@ -162,18 +165,21 @@ const InitialSettingsPage = ({
<ScrollView style={[styles.settingsPageBackground]} bounces={false}>
<View style={styles.w100}>
<View style={styles.pageWrapper}>
<View style={[styles.mb3]}>
<Pressable style={[styles.mb3]} onPress={openProfileSettings}>
<AvatarWithIndicator
size="large"
source={myPersonalDetails.avatar}
isActive={network.isOffline === false}
/>
</View>
<Text style={[styles.displayName, styles.mt1]} numberOfLines={1}>
{myPersonalDetails.displayName
? myPersonalDetails.displayName
: Str.removeSMSDomain(session.email)}
</Text>
</Pressable>

<Pressable style={[styles.mt1]} onPress={openProfileSettings}>
<Text style={[styles.displayName]} numberOfLines={1}>
{myPersonalDetails.displayName
? myPersonalDetails.displayName
: Str.removeSMSDomain(session.email)}
</Text>
</Pressable>
{myPersonalDetails.displayName && (
<Text
style={[styles.settingsLoginName, styles.mt1]}
Expand Down
156 changes: 156 additions & 0 deletions src/pages/workspace/WorkspaceEditorPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import _ from 'underscore';
import ONYXKEYS from '../../ONYXKEYS';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import ScreenWrapper from '../../components/ScreenWrapper';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import Navigation from '../../libs/Navigation/Navigation';
import Permissions from '../../libs/Permissions';
import styles from '../../styles/styles';
import TextInputWithLabel from '../../components/TextInputWithLabel';
import Button from '../../components/Button';
import Text from '../../components/Text';
import compose from '../../libs/compose';
import {
uploadAvatar, update,
} from '../../libs/actions/Policy';
import Icon from '../../components/Icon';
import {Workspace} from '../../components/Icon/Expensicons';
import AvatarWithImagePicker from '../../components/AvatarWithImagePicker';
import defaultTheme from '../../styles/themes/default';

const propTypes = {
/** List of betas */
betas: PropTypes.arrayOf(PropTypes.string),

...withLocalizePropTypes,
};
const defaultProps = {
betas: [],
};

class WorkspaceEditorPage extends React.Component {
constructor(props) {
super(props);

this.state = {
name: props.policy.name,
avatarURL: props.policy.avatarURL,
previewAvatarURL: props.policy.avatarURL,
};

this.submit = this.submit.bind(this);
this.onImageSelected = this.onImageSelected.bind(this);
this.onImageRemoved = this.onImageRemoved.bind(this);
this.uploadAvatarPromise = Promise.resolve();
}

onImageSelected(image) {
this.setState({previewAvatarURL: image.uri});

// Store the upload avatar promise so we can wait for it to finish before updating the policy
this.uploadAvatarPromise = uploadAvatar(image).then(url => 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 <Navigation.DismissModal />;
}

if (_.isEmpty(policy)) {
return null;
}

return (
<ScreenWrapper>
<HeaderWithCloseButton
title={this.props.translate('workspace.editor.title')}
onCloseButtonPress={Navigation.dismissModal}
/>

<View style={[styles.pageWrapper, styles.flex1, styles.pRelative]}>
<View style={[styles.w100, styles.flex1]}>
<AvatarWithImagePicker
avatarURL={this.state.previewAvatarURL}
DefaultAvatar={() => (
<Icon
src={Workspace}
height={80}
width={80}
fill={defaultTheme.icon}
/>
)}
style={[styles.mb3]}
anchorPosition={{top: 172, right: 18}}
isUsingDefaultAvatar={!this.state.previewAvatarURL}
onImageSelected={this.onImageSelected}
onImageRemoved={this.onImageRemoved}
/>

<TextInputWithLabel
label={this.props.translate('workspace.editor.nameInputLabel')}
value={this.state.name}
onChangeText={name => this.setState({name})}
onSubmitEditting={this.submit}
/>
<Text style={[styles.mt2, styles.formHint]}>
{this.props.translate('workspace.editor.nameInputHelpText')}
</Text>
</View>

<Button
success
style={[styles.w100]}
text={this.props.translate('workspace.editor.save')}
onPress={this.submit}
pressOnEnter
/>
</View>
</ScreenWrapper>
);
}
}

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);
Loading