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

Redesign change user experience modal #3077

Merged
merged 72 commits into from Sep 13, 2018

Conversation

MichaelBuessemeyer
Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer commented Aug 20, 2018

In this PR the modal to change the experience of one or multiple users got a rework.
Now there is a difference between editing multiple users' and a single user's experience.

single user

  • The modal used for editing a single user can be opened by clicking on one of the experience tag in the user table. Alternatively it can be opened by selecting only one user and pressing the "Change Experience"-button.
  • The modal should consist of two components. The first component is a table that lists all current experience domains of the user with their value. The first column shows the name of the experience domain. The second one shows the value in an editable NumberInput-field which accepts only values greater than 0. Editing the value of a experience domain makes a icon right next to the input-field visible. Pressing this Icon should reset the value to the initial value. This feature is only available to domains that already existed before starting editing. With the column on the right it is possible to remove a experience domain and also to undo this change. Removed entries should be disabled (except for the undo-icon).
  • If the table has more than 5 entries it, gets a scrollbar. With the scrollbar enabled antd changes the whole layout of the table (but this should be acceptable / I don't know how to fix this).
  • The second component is a input-field in which you can enter multiple new domains. Next to it is a add-button to add all entered domains. If a domain is entered, that is already listed it will be deleted from the input at the moment it is entered, so that there will never be two domains with the same name in the table. Also entering a domain with less than 3 characters is considered invalid => the input gets rejected and a toast should appear to inform the user.
  • The modal should not be closeable by clicking on the background. Pressing the submit button will submit all changes and the cancel button just closes the modal. Each time the modal gets visible the changes previous entered will be erased.

muliple users

  • Has three components:
  • A table that display all shared experience domains. If the value of each user of a domain is equal, only the value will be displayed in the value-column. Otherwise the text "varying from [lowest_value] to [highest_value]" will be displayed.
  • The next component is almost identical to the editing table in the single user's modal. All changes will be listed here. Adding a domain can be done via the third component the input-field. The difference here is that this table is smaller (height) than in the single user version. All users selected will be changed according to these entries. A listed domain, that is not marked as deleted will result in all users having this domain with the specified value when submitting. All users that have a domain that is marked as removed will have this domain removed after submitting. Also on the very right side of the table there is a button to remove the entered domain completely from the changes-table to make it possible to remove accidental entered changes.
  • The select-component for domains is the same as in the single users version but also has a "remove domains"-button that adds the entered domains to the change-table but directly set as removed.
    -Submitting the entered changes will result in the changes described in the third point.

general:

After submitting the changes the user-list-view should not fetch the users' data again. Instead it should use the result of the backend send by changing the experiences of a user.


# Edit 
## changes of the rework
- there is only one table in the modal. The shared experience table (showed when multiple users are sleected) got removed. Instead a new column was added to the other table to indicate the range of shared experience domains.
- while editing the exp of many users you can check a checkbox to only show and edit the shared  experiences of all users.
- clicking the trash icon will remove the entry completely
- there is no more "remove experience"-button 
- just entering a experience in the select field will add it to the table
- revert changes button still exist
- the title of the table got removed

### URL of deployed dev instance (used for testing):
- [https://redesignchangeuserexperiencemodal.webknossos.xyz](https://redesignchangeuserexperiencemodal.webknossos.xyz)

### Steps to test:
- open the users tab of the admins views
- change experience domains as you like from a single user and a multiple users
- test if the usage is somehow unintuitive
- check for unintended behavior 

### Issues:
- fixes [#3033](https://github.com/scalableminds/webknossos/issues/3033)

------
- [x] Updated [changelog](../blob/master/CHANGELOG.md#unreleased)
- ~~~[ ] Updated [migration guide](../blob/master/MIGRATIONS.md#unreleased) if applicable~~~
- ~~~[ ] Needs datastore update after deployment~~~
- [x] Ready for review

…s is now fully clearable; added button next to input to clear it
…ctionality is still missing; fixed table displaying errors
added submit functionality to modal that changes the experience of multiple users and enabled negative input to add possibility of shared decrease in exp domains
@MichaelBuessemeyer MichaelBuessemeyer changed the title Redesign change user experience modal [WIP] Redesign change user experience modal Aug 20, 2018
@philippotto philippotto self-requested a review September 10, 2018 09:12
Copy link
Member

@philippotto philippotto left a comment

Choose a reason for hiding this comment

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

Very nice work 👍 I see that you put a lot of effort into this PR and it already feels like a huge improvement over the old dialog 🎉

However, there are some things which we need to get done before merging, I think:

  • As far as I understand, it's currently not possible to not overwrite "varying" experience domains when editing multiple users. Is this correct? I'd suggest to leave the input fields empty for such cases and simply not touch these domains if the user didn't input any change.
  • I managed to break the dialog several times (whole page turned white). I think I did the following: select multiple users for editing, change some domain, click revert and then click delete. I think I also found the reason for this (see my review comments for that).
  • Code wise, I made some comments which you can just follow. The only "bigger" thing is that there are a lot if if-else branches for the single vs. multiple mode. Ideally, we would avoid those. I'd hope that the multiple mode could just cover the single mode so that we don't need to handle the single mode as an extra case. Do you think that's possible?

this.setState(prevState => ({
isExperienceModalVisible: false,
users: prevState.users.map(
user => updatedUsers.find(currentUser => currentUser.id === user.id) || user,
Copy link
Member

Choose a reason for hiding this comment

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

For the sake of performance, I'd replace the find method with something that has a near-constant look-up time. You can construct an object which maps from user id to the corresponding updated user with const updatedUsersMap = _.keyBy(updatedUsers, u => u.id). Then, you can replace the find-call with updatedUsersMap[user.id]. The code would then look like this:

this.setState(prevState => {
  const updatedUsersMap = _.keyBy(updatedUsers, u => u.id);
  return {
    isExperienceModalVisible: false,
    users: prevState.users.map(user => updatedUsersMap[user.id] || user)
  };
});

(I didn't test the code, so please incorporate it with care :))

this.setState({ domains: await getExistingExperienceDomains() });
}

removeAlreadyUsedDomains(): ExperienceDomainListType {
Copy link
Member

Choose a reason for hiding this comment

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

Please rename this method to getUnusedDomains. The current name suggests that data is mutated, but this is not the case :)


const { Column } = Table;

// TODO => visual bug when editing a value of a domain the first time !!!
Copy link
Member

Choose a reason for hiding this comment

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

Is this todo still valid?

title="Current Experience Value"
width="30%"
render={(record: MultipleUserTableEntry) =>
// eslint-disable-next-line no-nested-ternary
Copy link
Member

Choose a reason for hiding this comment

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

Eslint rules should only be disabled for a good reason :) Avoiding nested ternaries improves readability and could look like this:

if (record.highestValue === -1 && record.lowestValue === -1) {
  return "";
}
return record.highestValue === record.lowestValue ? record.highestValue : `varying ...`;

className="experience-change-modal"
title={
mutlipleUsers
? "Changes Experiences of Multiple Users"
Copy link
Member

Choose a reason for hiding this comment

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

I'd change this to ... of Selected Users.

updatedAllUsers = async () => {
let newUserPromises: Array<Promise<APIUserType>>;
if (this.state.showOnlySharedExperiences && this.props.selectedUsers.length > 1) {
const relevantEntries = this.state.multipleUsersEntries.filter(entry => entry.isShared);
Copy link
Member

Choose a reason for hiding this comment

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

I'd rename this to sharedEntries since this is clearer.

const relevantEntries = this.state.multipleUsersEntries.filter(entry => entry.isShared);
newUserPromises = this.props.selectedUsers.map(user => {
const newExperiences = { ...user.experiences };
relevantEntries.forEach(entry => {
Copy link
Member

Choose a reason for hiding this comment

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

Here, you are initializing newExperiences and then you update it in a forEach call. In general, it's nicer if you can define a variable directly with the right values so that you don't need any mutation. In this case, you can do something like this:

const newExperiences = { 
	...user.experiences,
	..._.fromPairs(relevantEntries.map({domain, value} => [domain, value]))
};

_.fromPairs takes something like [["a", "b"], ["c", "d"]] and returns {a: "b", c: "d"}. With the ... spread, the new experiences are directly merged into the existing ones.

});
// sort entries
return sharedTableEntries.sort(
Utils.localeCompareBy(([]: Array<MultipleUserTableEntry>), entry =>
Copy link
Member

Choose a reason for hiding this comment

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

This sort code appears four times in this module. Can you extract this into a helper method so that you only need to write sort(sharedTableEntries)?

}
});
// create sharedTableEntries
sharedTableEntries = sharedTableEntries.map(entry => {
Copy link
Member

Choose a reason for hiding this comment

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

Why is the variable named sharedTableEntries if it can contain entries with isShared == false? (line 95)


const newUser = update(user, {
experiences: { [domain]: { $set: newExperienceLevel } },
loadMultipleUserEntries = (users: Array<APIUserType>): Array<MultipleUserTableEntry> => {
Copy link
Member

Choose a reason for hiding this comment

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

I find this method a bit hard to understand. Can you try to make more use of lodash's helper functions? For example, if you want to find out which experience domains are shared you can write something similar to:

_.intersection(...users.map(user => Object.keys(user.experiences)))

When you want to find out the min/max value for a certain domain you can leverage _.min/_.max etc.

@MichaelBuessemeyer
Copy link
Contributor Author

MichaelBuessemeyer commented Sep 11, 2018

@philippotto The code should be working now and ready for another review. I could not find a unwanted behaviour or bug in it 😃

Copy link
Member

@philippotto philippotto left a comment

Choose a reason for hiding this comment

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

Awesome, the refactored code looks really good 👍 I left some more comments, though 🙈

domain,
value,
lowestValue: -1,
highestValue: -1,
Copy link
Member

Choose a reason for hiding this comment

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

lowestValue and highestValue are probably not used for the "single-user mode", but wouldn't it make more sense to write value into these variables?

Copy link
Member

Choose a reason for hiding this comment

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

But -1 probably means that the interval is not rendered in the table, right? If you add the comments to value, lowestValue, highestValue which I mentioned somewhere else, this will probably clear it up for me :)


this.closeModal(newUserPromises);
loadTableEntries = (users: Array<APIUserType>): Array<TableEntry> => {
Copy link
Member

Choose a reason for hiding this comment

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

I'd avoid the word load here, since it sounds as if this data is fetched from the server (that's why functions starting with load usually return a promise). How about aggregateTableEntries or just getTableEntries?

const allSharedDomains: Array<string> = _.intersection(
...users.map(user => Object.keys(user.experiences)),
);
const allDomains: Array<string> = _.union(...users.map(user => Object.keys(user.experiences)));
Copy link
Member

Choose a reason for hiding this comment

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

Nice 👍

updateAllUsers = async () => {
let relevantEntries = this.state.tableEntries.filter(
entry =>
entry.changed || (entry.lowestValue === entry.highestValue && entry.highestValue >= 0),
Copy link
Member

Choose a reason for hiding this comment

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

Why is the second condition (low === high && high >= 0) necessary? Shouldn't the changed flag be enough to decide whether the entry is "relevant"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct, this actually produces a bug 😅 => resolved

if (!this.props.visible) {
return null;
setValueOfEntry = (index: number, value: number) => {
if (value >= 0) {
Copy link
Member

Choose a reason for hiding this comment

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

If you invert the condition here, you can have a flow like this:

if (value < 0) {
  return;
}
this.setState...

This removes one indention level from the function and makes it easier to read :)

app/assets/javascripts/admin/user/experience_modal_view.js Outdated Show resolved Hide resolved
visible={this.props.visible}
onCancel={this.props.onCancel}
width={600}
maskClosable={false}
Copy link
Member

Choose a reason for hiding this comment

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

The default for maskClosable is false, which is why you can remove this line.

app/assets/javascripts/admin/user/experience_modal_view.js Outdated Show resolved Hide resolved
app/assets/javascripts/admin/user/experience_modal_view.js Outdated Show resolved Hide resolved

type TableEntry = {
domain: string,
value: number,
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a comment here which explains in which cases value, lowestValue and highestValue are -1? This variable denotes a special meaning, right? It's not yet clear to me, when -1 is used, so it would be cool to explain it here.

@philippotto
Copy link
Member

Great 🎉 I also asked the lab for some feedback and in general they liked it pretty good. The following two minor requests came up:

"Is shared" is very helpful, in addition could you show the number of users that have that exp. domain already instead of the check ?

I don’t think we need the show only shared exp. domains checkbox since that is already very nicely shown in “Is shared”

Also I have a suggestion for renaming the table headers:

  • Drop the "Experience" part (so, it only says domain / value)
  • Drop "Delete entry" in the last column (just leave the column name empty, since the icon in the rows is self explanatory)
  • In the multi-user mode, there is the "current value" column and the "value" column. I'd rename the latter one to "new value"

@MichaelBuessemeyer
Copy link
Contributor Author

@philippotto Adding the number to the isShared column works already. But I still kept the icon, because the user might not know how many users he selected in total. So I got two suggestions:

  • keep it as it is with the icon and the number of users that have this domain
  • or just keep the number (and remove the icon) and add the count of total selected users to the title (e.g. Changes Experiences of {countOfSelectedUsers} Users

@MichaelBuessemeyer
Copy link
Contributor Author

@philippotto This PR should be ready for another round 🙂

@philippotto
Copy link
Member

@philippotto This PR should be ready for another round slightly_smiling_face

Nice, this looks and feels really good 👍 I'm very happy with how this turned out.

or just keep the number (and remove the icon) and add the count of total selected users to the title (e.g. Changes Experiences of {countOfSelectedUsers} Users

Yes, this is a good idea! Since I wasn't quite sure what would "feel good" in this case, I fiddled a bit around with the code (and also committed it, since I was already at it) and went for a colorful badge, which is green if the domain is shared (otherwise it's white). The badge also has a tooltip explaining what's going on.
I also renamed the column to "user count" which hopefully conveys (together with the tooltip) that it shows the "count of users with that domain".

What do you think? Is this easy to understand from a user perspective? If so, I think this is ready to land 🎉

Looks like this:
image

@MichaelBuessemeyer MichaelBuessemeyer merged commit ace1781 into master Sep 13, 2018
@philippotto philippotto deleted the redesign-change-user-experience-modal branch September 18, 2018 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants