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

[MM-62701] [MM-62176]Edit custom profile attributes in user profile #8557

Merged
merged 2 commits into from
Feb 19, 2025

Conversation

Willyfrog
Copy link
Contributor

@Willyfrog Willyfrog commented Feb 4, 2025

Summary

Add custom profile attributes to user profile and be able to edit them.

Ticket Link

MM-62701

MM-62176

Checklist

  • Added or updated unit tests (required for all new features)
  • Has UI changes
  • Includes text changes and localization file updates
  • Have tested against the 5 core themes to ensure consistency between them.
  • Have run E2E tests by adding label E2E iOS tests for PR.

Device Information

This PR was tested on: iOS emulator iPhone and iPad, Pixel 8

Screenshots

CleanShot 2025-02-06 at 15 46 18@2x

Release Note

Add custom profile attributes to the edit profile screen

@Willyfrog Willyfrog changed the title [MM-62701] Edit custom profile attributes in user profile [MM-62701] [MM-62176]Edit custom profile attributes in user profile Feb 4, 2025
@@ -224,7 +224,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
}

return res;
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, multilineInputHeight, editable]);
}, [styles, textInputStyle, multiline, multilineInputHeight]);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

IDE was complaining regarding dependencies. There are others like this throughout the PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do check the implications of each one of the removed ones

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding is that we want to re-render on those cases, I'll add a comment so there is no warning and it is clear that is intentional.

On the other instances I only added dependencies that are in use within the block, so I believe they are ok. Let me know if you think otherwise :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Unless something weird is being done (and therefore, the problem is doing that weird thing) you should be able to trust the linter on any useMemo or useCallback. The dependencies stated there should be the ones you need.

With useEffect is more complicated, since the way useEffect is generally used, you don't always want to execute the effect when something inside changes. So those cases you have to look a bit more careful.

@Willyfrog Willyfrog requested review from larkox and enahum February 6, 2025 11:54
@Willyfrog Willyfrog marked this pull request as ready for review February 6, 2025 11:55
@Willyfrog Willyfrog added 2: Dev Review Requires review by a core commiter 3: QA Review Requires review by a QA tester labels Feb 7, 2025
@Willyfrog Willyfrog added this to the v2.26.0 milestone Feb 7, 2025
Copy link
Contributor

@larkox larkox left a comment

Choose a reason for hiding this comment

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

Nothing really blocking on my comments, but refraining to approve yet in wait for the changes we discussed around the custom_profile_field.tsx file.

Comment on lines 246 to 257
{Object.entries(userInfo.customAttributes || {}).map(([id, customAttr]) => (
<CustomProfileField
key={id}
fieldKey={`customAttributes.${id}`}
label={customAttr.name}
maxLength={128}
{...fieldConfig}
returnKeyType='done'
testID={`edit_profile_form.custom_attribute.${id}`}
value={customAttr.value}
/>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

Sanity check:

Do we ever expect so many custom fields (or so heavy) that we want to virtualize this list?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right now the max allowed is 20, but there were talks where you could get custom attributes from server + teams + channels. Not sure if we'll ever get that far, but if we do, we can easily go over 50 on complex environments

Copy link
Contributor

Choose a reason for hiding this comment

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

50 might be still manageable, but it would be great to do a test, to try to see when it starts to become a problem (also keeping in mind whether you are using a low, med, or high-end device).

A virtualized list comes with its own overhead, and I am not sure how many elements we will be able to hide (if in the screen you can fit around 10, the virtualized list is going to have loaded at least 20 or 30, depending on the configuration), so I don't want to do any early optimization here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the posibiliity is there in the future, once we start adding new types that might require front-end calculation.
but at the same time, in my list of tasks is moving everything to watermelondb which might be able to store any intermediate results if needed.

};

fetchCustomAttributes();
}, [currentUser, serverUrl]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This will never be an issue, but we are opening the door for it being an issue :P

This is not going to be a problem because currentUser and serverUrl will never change. But let's imagine it does.

If that was the case, we would make a request every time any of these values change. Since we are overwriting the value of customAttributes, it should be fine, unless... we have two calls done in a short time, and there is a race condition. In that case, we may ovewrite customAttributes with wrong information.

There is also the detail that if one of the requests fail, we may keep stale data, since we don't clean on throw.

How I would solve this for now? Empty the dependency list. This is an "onMount". We don't want to do this again on update ever.

Another solution? Add a timestamp on a ref, so you only store (or clean on) "the latest call".

The reality? This can stay like this, because nothing of what I said is going to cause any real problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To be fair, I added it to make the linter happy, calculations depend on them and didn't think too much of that.

We can remove the serverURL, but I think we should still keep the currentUser in case the session is killed, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the session is killed, we get directly logged out, and kicked to the login screen. So we should never be in this screen while the user is changing.

But as I said... this is just a philosophical issue, not a really technical one :P

Comment on lines 199 to 200
const client = NetworkManager.getClient(serverUrl);
await client.updateCustomProfileAttributeValues(values);
Copy link
Contributor

Choose a reason for hiding this comment

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

We may want to add this to a remote action, to avoid having to deal with the network manager at this level. Also, it will handle the possible thrown exceptions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, if there is any error, I wonder if we want to inform the user somehow.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see tests for the new behavior on this file.

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 tried to add one to this, but couldn't get it to work. Since there are a ton of mechanisms in place for rendering this piece, I skipped it for the time being. Likely an issue on my end, but since there are already tests for the form section, that should cover most functionality along with the remote action ones.

@amyblais amyblais modified the milestones: v2.26.0, v2.27.0 Feb 12, 2025
@Willyfrog Willyfrog requested a review from larkox February 14, 2025 11:21
Copy link
Contributor

@larkox larkox left a comment

Choose a reason for hiding this comment

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

Please check the comment about the getRef on the hook. Apart of that, the rest are nitpicks.

});
});

it('fetchCustomAttributes - no fields', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: You are missing the network error test.
Also... why not putting all the tests for the same function under one describe?

Copy link
Contributor

Choose a reason for hiding this comment

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

You can do

describe('Custom Profile Attributes')
  describe('fetchCustomAttributes')
    it('base case')
    it('no fields')
...

Object.keys(attributes).forEach((field) => {
values[field] = attributes[field].value;
});
await client.updateCustomProfileAttributeValues(values);
Copy link
Contributor

Choose a reason for hiding this comment

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

Sanity check: Any error (like forbidden, for example) would go through the catch path, right?

expect(result.attributes).toEqual({});
});

it('updateCustomAttributes - base case', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: You are missing the error case here too.

}
return {attributes: {}};
} catch (error) {
logDebug('error on fetchCustomAttributes', getFullErrorMessage(error));
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably want to add here the logout if needed function.

@@ -224,6 +224,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
}

return res;
// eslint-disable-next-line react-hooks/exhaustive-deps
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you want to skip this in this memo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the linter is complaining and seems we want to update when anything regarding styling is changed. see #8557 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

But this memo is to deal with styles. We want anything regarding styles to re-calculate this, right?

return allRefs.current;
};

const numRefs = () => getAllRefs().size;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we want this? Is this just for testing purposes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was an attempt to figure out why it wasn't rendering properly. After deciding for a different approach I thought about removing it, but I found that it was good for debug/testing purposes and left it in. Since it is the last element, it can be easily skipped.

Copy link
Contributor

Choose a reason for hiding this comment

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

The wider the interface, the wider the maintenance area. I would say we remove it, just to avoid having something extra to maintain, but I see your point regarding testing. 0/5

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Comment on lines 31 to 33
const getRef = (key: string): FloatingTextInputRef | undefined => {
return allRefs.current?.get(key);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Being purist, we should try all the returns of the hook to be "stable" (same function if you get the same values). But getRef is more important, because it is more common to have it in other callbacks, and it might cause unneeded re-renders.

Long story short, use useCallback at least for this one (I would prefer for all the returns).

} else {
Keyboard.dismiss();
}
}), [canSave, userProfileFields]);
}), [canSave, userProfileFields, submitUser, getRef]);
Copy link
Contributor

Choose a reason for hiding this comment

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

See here that we are using getRef in a dependency. If we don't make it stable, this callback becomes useless, and may be rendering more callbacks useless down the line.

@Willyfrog Willyfrog requested a review from larkox February 17, 2025 16:42
Copy link
Contributor

@larkox larkox left a comment

Choose a reason for hiding this comment

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

No more blocking comments from my side.

@Willyfrog Willyfrog requested a review from lindy65 February 18, 2025 10:12
@Willyfrog Willyfrog added the Build Apps for PR Build the mobile app for iOS and Android to test label Feb 18, 2025
@Willyfrog
Copy link
Contributor Author

@enahum friendly reminder to review :)

email: string;
firstName: string;
lastName: string;
nickname: string;
position: string;
username: string;
customAttributes: Record<string, CustomAttribute>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well make

export interface CustomAttributes {
  [key: string]: CustomAttribute;
}

Then you can reuse this in a few instances in the user.ts file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added, I also declared the return type which showed a weird return in one of the cases :)

@Willyfrog
Copy link
Contributor Author

/update-branch

Copy link
Contributor

@enahum enahum left a comment

Choose a reason for hiding this comment

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

I'm not very comfortable having to fetch the custom attributes every time the screen is loaded and have them ephemeral, but I won't block the feature

@@ -878,3 +879,46 @@ export const getAllSupportedTimezones = async (serverUrl: string) => {
return [];
}
};

export const fetchCustomAttributes = async (serverUrl: string, userId: string): Promise<{attributes: CustomAttributeSet; error: unknown}> => {
Copy link
Contributor

Choose a reason for hiding this comment

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

These attrs are always ephemeral?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, the plan is (at some point) add everything to the database and handle it from there. Due to how the feature was developed that was deprioritized.

@lindy65 lindy65 requested a review from asaadmahmood February 19, 2025 09:52
@lindy65 lindy65 added the 2: UX Review Requires review by a UX Designer label Feb 19, 2025
@Willyfrog
Copy link
Contributor Author

I'm not very comfortable having to fetch the custom attributes every time the screen is loaded and have them ephemeral, but I won't block the feature

The idea is to move to a database-backed implementation, but when we started there was a ton of missing stuff missing in the backend. It'll be added in subsequent PRs if everything goes according to plan

@lindy65
Copy link

lindy65 commented Feb 19, 2025

Hi @Willyfrog

Not sure whether this in within the scope of this PR...

  1. Do you know which UI should be used on the "Your Profile" screen?

I'm seeing this from Figma on the Jira ticket:
image

And this in the app / build from this PR:
image

The UI is quite different actually

  • screen header is different
  • the word Save/SAVE is different case
  • In Figma the message is above the profile pic, in the app build, the text is below the profile pic
  1. Should the text in a long property description field wrap when focus is not within the field? Not all text that was typed into the field is visible whereas on webapp it is
Long.property.description.-.mobile.mp4
image image

Maybe these are questions for @asaadmahmood

@asaadmahmood
Copy link

@lindy65 @Willyfrog

  1. We should add the toast at the top if possible and we can even make it undismissable. The header can be different based on IOS/Android, we should just use whatever pattern is being currently used in the app, so no change there unless this deviates from the standard in the app right now.

  2. I'm assuming it would be hard to wrap, because we do not support textareas right now, so I do not mind the way it is, but on desktop here we should atleast add an ellipsis.
    CleanShot 2025-02-19 at 15 41 27@2x

@Willyfrog
Copy link
Contributor Author

@asaadmahmood is it ok to add that banner in a separate PR? I already have more changes waiting on this PR being merged.

For multiline support, I was waiting for adding the new types and see if we can differentiate text uses (short vs large) to use one or another instead of forcing everything to be multiline (which might not look pretty when used for small texts)

@asaadmahmood
Copy link

@Willyfrog Sure, if we do not miss that (the banner).

@Willyfrog
Copy link
Contributor Author

ticket created: https://mattermost.atlassian.net/browse/MM-63216

@lindy65
Copy link

lindy65 commented Feb 19, 2025

@lindy65 @Willyfrog

  1. We should add the toast at the top if possible and we can even make it undismissable. The header can be different based on IOS/Android, we should just use whatever pattern is being currently used in the app, so no change there unless this deviates from the standard in the app right now.
  2. I'm assuming it would be hard to wrap, because we do not support textareas right now, so I do not mind the way it is, but on desktop here we should atleast add an ellipsis.
    CleanShot 2025-02-19 at 15 41 27@2x
  1. 👍
  2. 👍 and I think there is an elipses in desktop that was added in this ticket - I just don't think that code is included here

Copy link

@lindy65 lindy65 left a comment

Choose a reason for hiding this comment

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

Hey @Willyfrog - based on Asaad's comments and your having created a separate ticket for the banner, I'm happy to go ahead and approve

@lindy65 lindy65 added QA Review Done and removed 3: QA Review Requires review by a QA tester 2: Dev Review Requires review by a core commiter labels Feb 19, 2025
@Willyfrog Willyfrog force-pushed the edit_profile_cpa branch 2 times, most recently from 06043e9 to c98bf5a Compare February 19, 2025 12:32
feat: Add support for custom profile attributes in edit profile

refactor: Normalize whitespace in CustomAttribute type definition

fix: Resolve type mismatch for customAttributes in UserInfo interface

test: Add test for udpateCustomProfileAttributeValues method

fix typing, submit changes to server

missing files

test: Add tests for CustomProfileField component

fix naming

fix imports

fix

feat: Add field_refs hook for managing field references

feat: Make `setRef` ref parameter optional with default no-op implementation

refactor: Replace CustomProfileField with useFieldRefs for profile form

refactor: Optimize edit profile screen imports and custom attributes handling

refactor: Move custom attributes logic to remote actions in user.ts

address PR reviews

test: Add tests for custom attributes in edit profile

test: Add tests for EditProfile component with custom attributes

fix: Add UserModel type assertion to currentUser in edit profile tests

test: Add tests for ProfileForm custom attributes functionality

test: Add comprehensive tests for useFieldRefs hook

test: Add tests for fetchCustomAttributes and updateCustomAttributes

add tests

remove unneeded files

review changes

remove counter from hook

remove package.resolved

create interface for reuse of record
@Willyfrog
Copy link
Contributor Author

Willyfrog commented Feb 19, 2025

I had to fiddle around to remove the commits and go back to the original code to make sure the latest changes from main are there

@Willyfrog Willyfrog merged commit 0addf49 into main Feb 19, 2025
6 checks passed
@Willyfrog Willyfrog deleted the edit_profile_cpa branch February 19, 2025 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2: UX Review Requires review by a UX Designer Build Apps for PR Build the mobile app for iOS and Android to test QA Review Done release-note
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants