Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the WorkspaceSettingsPage #6033

Merged
merged 25 commits into from
Oct 26, 2021
Merged

Refactor the WorkspaceSettingsPage #6033

merged 25 commits into from
Oct 26, 2021

Conversation

mountiny
Copy link
Contributor

@mountiny mountiny commented Oct 25, 2021

cc @HorusGoul @tgolen

Details

This PR refactors some bad smells:

  1. It moves API actions from WorkspaceSettingsPage.js to actions/Policy.js
  2. The actions do not return Promise anymore as this is not required in this case
  3. Use Onyx as source of truth

Additionally, I have added a small change in the flow (which we can discuss if we want to keep) which is the avatar being saved in the policy when it is uploaded. Currently, user needs to upload the avatar, wait for it to load and then click Save to actually save the avatar into the policy.

After this refactoring, the avatar flow is independent on the name/currency flow, so the new picture is saved to our database once it is uploaded.

Fixed Issues

$ https://github.com/Expensify/Expensify/issues/180546

Tests

This is a refactoring of the Workspace settings editor. We need to make sure uploading a new avatar for workspace or changing name/currency still works as expected.

  1. Go to your workspace's General settings.
  2. Click on the avatar and pick some image to use as avatar and upload it.
  3. Success Growl message should show indicating the upload has been successful.
  4. Exit the right hand side panel, reload the page (potentially even clearing your cache) and make sure the workspace has the new picture as its avatar.
  5. Go back to the workspace settings and change name of the workspace. Click Save.
  6. Exit the setting page, reload the page and make sure the name of the settings is changed.
  7. Lastly, go back to the settings page, click on the Avatar and remove it. Make sure it is removed and the placeholder image is shown (even after reloading).

QA Steps

Same as tests above.

Tested On

  • Web
  • Mobile Web
  • Desktop
  • iOS
  • Android

Screenshots

Web

Screen.Recording.2021-10-25.at.19.28.09.mov

Mobile Web

Desktop

iOS

Android

@mountiny mountiny self-assigned this Oct 25, 2021
Copy link
Contributor Author

@mountiny mountiny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ready for a review! Thank you very much for your time reviewing this.

I prefer this flow when the avatar image is saved immediately and users does not have to click the Save button in order to save the image. I think this is better UX as will more likely be times when users adds the avatar and notices it being uploaded and exits the settings without saving just to be confused that the image has actually not been saved.

On the other hand, I cannot imagine situation when users would be confused that the picture has been uploaded before they clicked Save.

However, I am open for discussion and happy to change this if you will think this approach is worse.

.then((avatarURL) => {
// Update the policy with the new avatarURL as soon as we get it
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {avatarURL, isAvatarUploading: false});
update(policyID, {avatarURL}, true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am now updating the policy immediately when we get the s3bucket url so the avatar is saved straight away.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this! I think this is a much better UX as well. When I first tested out this UI I thought it was a bug that the avatar wasn't saving on upload.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, I felt the same!

.then((response) => {
if (response.jsonCode !== 200) {
// Show the user error feedback
throw new Error();
Copy link
Contributor Author

@mountiny mountiny Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I am now chaining two .then() together, I could not just simply return here as before (that would be supplied to the next then statement), so I have decided to throw new Error, which will be then caught in the .catch().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering this... and I think it's mostly OK, except I don't really like that there are multiple .then()s being chained together. This makes the code harder to follow and more complex than it needs to be. I think this can all be rewritten like this and it's more simple:

    API.User_UploadAvatar({file})
        .then((response) => {
            if (response.jsonCode === 200) {
                // Update the policy with the new avatarURL as soon as we get it
                Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {avatarURL: response.s3url, isAvatarUploading: false});
                update(policyID, {avatarURL: response.s3url}, true);
                return;
            }

            Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {isAvatarUploading: false});
            const errorMessage = translateLocal('workspace.editor.avatarUploadFailureMessage');
            Growl.error(errorMessage, 5000);
        });

The only change this makes is that there is no more catch() so there won't be any HTTP errors caught either (but I'm not sure we really want to be catching those here anyway, and most API calls don't catch those in the action layer).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I really like this approach, why have I not seen it before 😅 Thanks for bringing this up, it is way clearer!

src/pages/workspace/WorkspaceSettingsPage.js Show resolved Hide resolved
src/pages/workspace/WorkspaceSettingsPage.js Show resolved Hide resolved
@mountiny mountiny changed the title [WIP] Refactor the WorkspaceSettingsPage Refactor the WorkspaceSettingsPage Oct 25, 2021
@mountiny mountiny requested a review from a team October 25, 2021 19:10
@mountiny mountiny marked this pull request as ready for review October 25, 2021 19:11
@MelvinBot MelvinBot requested review from sketchydroide and removed request for a team October 25, 2021 19:11
@parasharrajat
Copy link
Member

Please hold, one of my PR is going to be merged soon #5271 which handles multiple issues. It's on hold since N6.

@mountiny mountiny changed the title Refactor the WorkspaceSettingsPage [HOLD #5271] Refactor the WorkspaceSettingsPage Oct 25, 2021
@mountiny
Copy link
Contributor Author

@parasharrajat Thank you for heads up! I will need to resolve the merge conflicts first then. I guess this can wait for a review :)

@parasharrajat
Copy link
Member

parasharrajat commented Oct 25, 2021

@mountiny Sorry, it is just merged. 🤣

Copy link
Contributor

@tgolen tgolen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much better, Vit! Thank you.

})).catch(() => {
Growl.error(this.props.translate('workspace.editor.avatarUploadFailureMessage'));
}).finally(() => Policy.updateLocalPolicyValues(this.props.policy.id, {isAvatarUploading: false}));
Policy.updateLocalPolicyValues(this.props.policy.id, {isAvatarUploading: true});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the three instances we have of Policy.updateLocalPolicyValues() are all immediately before calling something that updates the policy. I think that this is confusing and also error-prone. What I would suggest is see about making Policy.updateLocalPolicyValues() a local method that is not exported, and then just reference it internally inside the action (inside of Policy.update() and Policy.uploadAvatar()).

I think that keeping it internal will make the view code more simple (only have to call one method) and less error prone (don't have to worry about forgetting to call one of the methods).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is also a good point and I agree that it will be clearer if we keep it in the Policy actions. I have refactored it accordingly :)

.then((response) => {
if (response.jsonCode !== 200) {
// Show the user error feedback
throw new Error();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering this... and I think it's mostly OK, except I don't really like that there are multiple .then()s being chained together. This makes the code harder to follow and more complex than it needs to be. I think this can all be rewritten like this and it's more simple:

    API.User_UploadAvatar({file})
        .then((response) => {
            if (response.jsonCode === 200) {
                // Update the policy with the new avatarURL as soon as we get it
                Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {avatarURL: response.s3url, isAvatarUploading: false});
                update(policyID, {avatarURL: response.s3url}, true);
                return;
            }

            Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {isAvatarUploading: false});
            const errorMessage = translateLocal('workspace.editor.avatarUploadFailureMessage');
            Growl.error(errorMessage, 5000);
        });

The only change this makes is that there is no more catch() so there won't be any HTTP errors caught either (but I'm not sure we really want to be catching those here anyway, and most API calls don't catch those in the action layer).

@mountiny mountiny requested a review from a team as a code owner October 26, 2021 12:01
@MelvinBot MelvinBot requested review from joelbettner and removed request for a team October 26, 2021 12:02
@mountiny
Copy link
Contributor Author

Some pullerbear bug here, sorry Joel for notification, unassigning you.

@mountiny mountiny removed the request for review from joelbettner October 26, 2021 12:03
@mountiny mountiny requested a review from tgolen October 26, 2021 12:08
Copy link
Contributor Author

@mountiny mountiny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgolen Thank you very much for your time reviewing this. I have addressed your comments and made the improvements. This is ready for another look 🙌

Comment on lines -78 to -81
componentWillUnmount() {
Policy.updateLocalPolicyValues(this.props.policy.id, {isAvatarUploading: false});
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We won't need this block, because the entire upload process is now component agnostic and the picture or workspace details will upload even if user quits the component.

}

submit() {
if (this.props.policy.isAvatarUploading || !this.validate()) {
if (this.props.policy.isPolicyUpdating || !this.validate()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I have separated the image upload flow from the other policy details, we need to use isPolicyUpdating here.

Comment on lines +96 to 99
if (this.props.policy.isAvatarUploading) {
return;
}
this.setState({previewAvatarURL: image.uri});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, since we are now uploading and saving the ne picture immediately after users selects it from Desktop, we need to make sure they won't select new image while the previous one is still being uploaded.

@mountiny mountiny changed the title [HOLD #5271] Refactor the WorkspaceSettingsPage Refactor the WorkspaceSettingsPage Oct 26, 2021
Copy link
Contributor

@tgolen tgolen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, this looks really great now!! Nice work

@tgolen tgolen merged commit 1870921 into main Oct 26, 2021
@tgolen tgolen deleted the vit-refactoringOfBadSmells branch October 26, 2021 16:46
@OSBotify
Copy link
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@roryabraham
Copy link
Contributor

@mountiny Can you confirm if this is No QA? If so, please prefix the title with [No QA] 🙏 Thanks!

@mountiny
Copy link
Contributor Author

@roryabraham This should be QA'ed for any regressions, I have forgot to add mention to the QA part that it should be same as tests. Fixed it now.

@OSBotify
Copy link
Contributor

🚀 Deployed to staging by @tgolen in version: 1.1.10-3 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 failure ❌
🕸 web 🕸 success ✅

@OSBotify
Copy link
Contributor

🚀 Deployed to production by @roryabraham in version: 1.1.11-1 🚀

platform result
🤖 android 🤖 success ✅
🖥 desktop 🖥 success ✅
🍎 iOS 🍎 success ✅
🕸 web 🕸 success ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants