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

React version of UserEdit #3354

Merged
merged 44 commits into from
Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8c2aeda
Update DynamicForm export
gabrieldutra Jan 26, 2019
16361a4
Move UserShow to users folder
gabrieldutra Jan 26, 2019
afdceff
Migrate User profile header and create DynamicForm for basic data
gabrieldutra Jan 26, 2019
8cfb157
Update UserShow to use UserProfile prop
gabrieldutra Jan 26, 2019
c456fb1
Add API Key input
gabrieldutra Jan 27, 2019
d4fc964
Add handler to regenerate API Key button
gabrieldutra Jan 28, 2019
2917e22
Handle user profile save
gabrieldutra Jan 28, 2019
e1624c9
Merge branch 'master' into user-edit
gabrieldutra Jan 28, 2019
fff4e7a
Add readOnly prop to DynamicForm and begin disabled user behavior
gabrieldutra Jan 28, 2019
d17266e
Add Change Password Modal
gabrieldutra Jan 29, 2019
576c3d0
Remove action buttons for disabled users
gabrieldutra Jan 29, 2019
d891df1
Add send password reset behavior
gabrieldutra Jan 31, 2019
0262e08
Merge remote-tracking branch 'fork/master' into user-edit
gabrieldutra Feb 2, 2019
fb1f571
Add minLength and password comparison to Password Modal
gabrieldutra Feb 2, 2019
7c6f11e
Resend Invitation button
gabrieldutra Feb 2, 2019
82237e7
Add Convert User Info
gabrieldutra Feb 3, 2019
f85668b
Fix UserShow test
gabrieldutra Feb 3, 2019
3a9d794
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 3, 2019
f5c4090
Some code updates
gabrieldutra Feb 3, 2019
6cf8b2d
Add enable/disable user button
gabrieldutra Feb 3, 2019
8b7336b
Add UserPolicy as an idea
gabrieldutra Feb 3, 2019
ddd9758
Remove UserPolicy
gabrieldutra Feb 3, 2019
f9984e0
Create Edit Profile spec
gabrieldutra Feb 4, 2019
f8bc318
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 4, 2019
e4f83ab
Move User profile screenshot to Edit Profile Spec
gabrieldutra Feb 4, 2019
80d5809
Add tests for saving user and changing password errors
gabrieldutra Feb 5, 2019
3522cdc
CC is back :) - Fix trailing spaces
gabrieldutra Feb 5, 2019
17ed37a
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 5, 2019
74a77c5
Add test for succesful password update
gabrieldutra Feb 5, 2019
ae5652e
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 5, 2019
a89ae71
A few improvements from code review
gabrieldutra Feb 8, 2019
99f317c
Remove Toggle User button when seeing your own profile
gabrieldutra Feb 8, 2019
20d4b7c
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 9, 2019
e2f5303
Create InputWithCopy
gabrieldutra Feb 9, 2019
e2ed0a4
Fix possible errors when network is off and improve Email not sent alert
gabrieldutra Feb 9, 2019
071c968
Add default response object for $http possible errors
gabrieldutra Feb 9, 2019
f046914
Changes in UserEdit
gabrieldutra Feb 10, 2019
389d9ee
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 10, 2019
0289923
Update UserEdit render behavior and styling
gabrieldutra Feb 10, 2019
2e8756f
Create ChangePasswordDialog and update UserEdit
gabrieldutra Feb 13, 2019
f6dfdab
Fix possible console error
gabrieldutra Feb 13, 2019
3851e61
Merge remote-tracking branch 'master' into user-edit
gabrieldutra Feb 13, 2019
d5a3c8f
Remove password match assertion from spec
gabrieldutra Feb 13, 2019
230f7e3
Fix typo
gabrieldutra Feb 14, 2019
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
25 changes: 15 additions & 10 deletions client/app/components/dynamic-form/DynamicForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { toastr } from '@/services/ng';
import { Field, Action, AntdForm } from '../proptypes';
import helper from './dynamicFormHelper';

export class DynamicForm extends React.Component {
export const DynamicForm = Form.create()(class DynamicForm extends React.Component {
static propTypes = {
fields: PropTypes.arrayOf(Field),
actions: PropTypes.arrayOf(Action),
feedbackIcons: PropTypes.bool,
readOnly: PropTypes.bool,
saveText: PropTypes.string,
onSubmit: PropTypes.func,
form: AntdForm.isRequired,
};
Expand All @@ -25,6 +27,8 @@ export class DynamicForm extends React.Component {
fields: [],
actions: [],
feedbackIcons: false,
readOnly: false,
saveText: 'Save',
onSubmit: () => {},
};

Expand Down Expand Up @@ -139,23 +143,25 @@ export class DynamicForm extends React.Component {

renderFields() {
return this.props.fields.map((field) => {
const [firstItem] = this.props.fields;
const [firstField] = this.props.fields;
const FormItem = Form.Item;
const { name, title, type } = field;
const fieldLabel = title || helper.toHuman(name);
const { feedbackIcons, readOnly } = this.props;

const formItemProps = {
key: name,
className: 'm-b-10',
hasFeedback: type !== 'checkbox' && type !== 'file' && this.props.feedbackIcons,
hasFeedback: type !== 'checkbox' && type !== 'file' && feedbackIcons,
label: type === 'checkbox' ? '' : fieldLabel,
};

const fieldProps = {
autoFocus: (firstItem === field),
autoFocus: (firstField === field),
className: 'w-100',
name,
type,
readOnly,
placeholder: field.placeholder,
'data-test': fieldLabel,
};
Expand Down Expand Up @@ -191,22 +197,21 @@ export class DynamicForm extends React.Component {
disabled: this.state.isSubmitting,
loading: this.state.isSubmitting,
};
const { readOnly, saveText } = this.props;
const saveButton = !readOnly;

return (
<Form layout="vertical" onSubmit={this.handleSubmit}>
{this.renderFields()}
<Button {...submitProps}>
Save
</Button>
{saveButton && <Button {...submitProps}>{saveText}</Button>}
{this.renderActions()}
</Form>
);
}
}
});

export default function init(ngModule) {
ngModule.component('dynamicForm', react2angular((props) => {
const UpdatedDynamicForm = Form.create()(DynamicForm);
const fields = helper.getFields(props.type.configuration_schema, props.target);

const onSubmit = (values, onSuccess, onError) => {
Expand All @@ -231,7 +236,7 @@ export default function init(ngModule) {
feedbackIcons: true,
onSubmit,
};
return (<UpdatedDynamicForm {...updatedProps} />);
return (<DynamicForm {...updatedProps} />);
}, ['target', 'type', 'actions']));
}

Expand Down
9 changes: 9 additions & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,12 @@ export const Action = PropTypes.shape({
export const AntdForm = PropTypes.shape({
validateFieldsAndScroll: PropTypes.func,
});

export const UserProfile = PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
profileImageUrl: PropTypes.string,
apiKey: PropTypes.string,
isDisabled: PropTypes.bool,
});
157 changes: 157 additions & 0 deletions client/app/components/users/UserEdit.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from 'react';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Input from 'antd/lib/input';
import Tooltip from 'antd/lib/tooltip';
import Modal from 'antd/lib/modal';
import { react2angular } from 'react2angular';
import { User } from '@/services/user';
import { currentUser } from '@/services/auth';
import { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';

export class UserEdit extends React.Component {
static propTypes = {
user: UserProfile.isRequired,
};

constructor(props) {
super(props);
this.state = { user: this.props.user, changePassword: false };
}

handleSave = (values, onSuccess, onError) => {
const data = {
id: this.props.user.id,
...values,
};

User.save(data, (user) => {
onSuccess('Saved.');
this.setState({
user: {
gabrieldutra marked this conversation as resolved.
Show resolved Hide resolved
id: user.id,
name: user.name,
email: user.email,
profileImageUrl: user.profile_image_url,
apiKey: user.api_key,
isDisabled: user.is_disabled,
},
});
}, (error) => {
onError(error.data.message || 'Failed saving.');
});
};

openChangePasswordModal = () => {
this.setState({ changePassword: true });
};

changePasswordModal() {
const fields = [
{ name: 'old_password', title: 'Current Password' },
{ name: 'password', title: 'New Password' },
{ name: 'password_repeat', title: 'Repeat New Password' },
].map(field => ({ ...field, type: 'password', required: true }));

return (
<Modal
visible={this.state.changePassword}
title="Change Password"
onCancel={() => { this.setState({ changePassword: false }); }}
footer={null}
destroyOnClose
>
<DynamicForm fields={fields} saveText="Update Password" onSubmit={this.handleSave} />
Copy link
Member

Choose a reason for hiding this comment

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

Interesting. I never thought we will use DynamicForm to render forms in the app (that are not based on backend properties). But I guess it makes sense... I would draw the line in cases where rendering with DynamicForm requires too many tweaks and it's better to use the components directly.

Copy link
Member Author

@gabrieldutra gabrieldutra Jan 29, 2019

Choose a reason for hiding this comment

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

My thoughts are that components are a concept to avoid reuse of code. Forms can be pretty annoying to handle, so I tried to make DynamicForm in a way it would be an easy-to-use Object-To-Form component.

As for backend properties (the biggest tweak so far haha), it currently needs some mapping (improving how this is done is on my plans when migrating Data Sources & Destinations Page).

The biggest problem I see is that it's a lot easier to break a page you were not expecting to, and that's where creating tests take part 🙂

</Modal>
);
}

regenerateApiKey = () => {
const doRegenerate = () => {
User.regenerateApiKey(this.state.user).then(({ data }) => {
if (data) {
const { user } = this.state;
this.setState({ user: { ...user, apiKey: data.api_key } });
}
});
};

Modal.confirm({
title: 'Regenerate API Key',
content: 'Are you sure you want to regenerate?',
okText: 'Regenerate',
onOk: doRegenerate,
maskClosable: true,
autoFocusButton: null,
});
};

renderApiKey() {
const { user } = this.state;

const regenerateButton = (
<Tooltip title="Regenerate API Key">
<Icon type="reload" style={{ cursor: 'pointer' }} onClick={this.regenerateApiKey} />
</Tooltip>
);

return (
<div>
<hr />
<label>API Key</label>
<Input addonAfter={regenerateButton} value={user.apiKey} readOnly />
</div>
);
}

render() {
gabrieldutra marked this conversation as resolved.
Show resolved Hide resolved
const { user } = this.state;

const formFields = [
{
name: 'name',
title: 'Name',
type: 'text',
initialValue: user.name,
required: true,
},
{
name: 'email',
title: 'Email',
type: 'email',
initialValue: user.email,
required: true,
},
];

return (
<div className="col-md-4 col-md-offset-4">
<img
alt="profile"
src={user.profileImageUrl}
className="profile__image"
width="40"
/>
<h3 className="profile__h3">{user.name}</h3>
<hr />
<DynamicForm fields={formFields} readOnly={user.isDisabled} onSubmit={this.handleSave} />
{this.changePasswordModal()}
{!user.isDisabled && (
<React.Fragment>
{this.renderApiKey()}
<hr />
<Button className="w-100 m-t-10" onClick={this.openChangePasswordModal}>Change Password</Button>
{currentUser.isAdmin && <Button className="w-100 m-t-10">Send Password Reset Email</Button>}
</React.Fragment>
)}
</div>
);
}
}

export default function init(ngModule) {
ngModule.component('userEdit', react2angular(UserEdit));
}

init.init = true;
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';

import { react2angular } from 'react2angular';
import { UserProfile } from '../proptypes';

export const UserShow = ({ name, email, profileImageUrl }) => (
export const UserShow = ({ user: { name, email, profileImageUrl } }) => (
<div className="col-md-4 col-md-offset-4 profile__container">
<img
alt="profile"
Expand All @@ -26,9 +25,7 @@ export const UserShow = ({ name, email, profileImageUrl }) => (
);

UserShow.propTypes = {
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
profileImageUrl: PropTypes.string.isRequired,
user: UserProfile.isRequired,
};

export default function init(ngModule) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import renderer from 'react-test-renderer';
import { UserShow } from './UserShow';

test('renders correctly', () => {
const component = renderer.create(<UserShow name="John Doe" email="john@doe.com" profileImageUrl="http://www.images.com/llama.jpg" />);
const user = {
name: 'John Doe',
email: 'john@doe.com',
profileImageUrl: 'http://www.images.com/llama.jpg',
};

const component = renderer.create(<UserShow user={user} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

72 changes: 3 additions & 69 deletions client/app/pages/users/show.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,18 @@

<div ng-show="selectedTab == 'profile'">
<div class="row">
<user-show ng-if="!(currentUser.isAdmin || currentUser.id == user.id)" name="user.name" email="user.email" profile-image-url="user.profile_image_url"></user-show>
<div class="col-md-4 col-md-offset-4 profile__container" ng-if="(currentUser.isAdmin || currentUser.id == user.id)">

<img ng-src="{{ user.profile_image_url }}" class="profile__image" width="40">

<h3 class="profile__h3">{{user.name}}</h3>

<hr>

<form class="form" name="userSettingsForm" ng-submit="updateUser(userSettingsForm)" novalidate>
<div class="form-group" ng-class="{ 'has-error': (userSettingsForm.name | showError )}">
<label class="control-label" for="name" >Name</label>
<input name="name" id="name" type="text" class="form-control" ng-model="user.name" required
ng-readonly="user.is_disabled">
<error-messages input="userSettingsForm.name" form="userSettingsForm"></error-messages>
</div>
<div class="form-group" ng-class="{ 'has-error': (userSettingsForm.email | showError )}">
<label class="control-label" for="email" >Email</label>
<input name="email" id="email" type="email" class="form-control" ng-model="user.email" required
ng-readonly="user.is_disabled">
<error-messages input="userSettingsForm.email" form="userSettingsForm"></error-messages>
</div>
<div class="form-group d-flex justify-content-between" ng-if="!user.is_disabled">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-default" ng-click="disableUser(user)">Disable</button>
</div>
</form>
<user-show ng-if="!(currentUser.isAdmin || currentUser.id == user.id)" user="userInfo"></user-show>
<user-edit ng-if="userInfo && (currentUser.isAdmin || currentUser.id == user.id)" user="userInfo"></user-edit>

<div class="col-md-4 col-md-offset-4 profile__container" ng-if="(currentUser.isAdmin || currentUser.id == user.id)">
<hr>

<div ng-if="user.is_disabled" class="d-flex justify-content-between align-items-center">
<strong>This user is disabled.</strong>
<button type="button" class="btn btn-primary" ng-click="enableUser(user)">Enable</button>
</div>

<div ng-if="!user.is_disabled">
<div class="form-group">
<label class="control-label">API Key</label>
<input type="text" class="form-control" value="{{user.api_key}}" size="44" readonly/>
</div>
<div class="form-group">
<button class="btn btn-default" ng-click="regenerateUserApiKey(user)" ng-disabled="disableRegenerateApiKeyButton">Regenerate</button>
</div>

<hr>

<button type="button" class="btn btn-default" ng-click="isCollapsed = !isCollapsed">Change password <span class="caret"></span></button>

<div uib-collapse="isCollapsed">
<div class="well">
<form class="form" name="userPasswordForm" ng-submit="savePassword(userPasswordForm)" novalidate>
<div class="form-group required" ng-class="{ 'has-error': (userPasswordForm.currentPassword | showError:userPasswordForm )}">
<label class="control-label">Current Password</label>
<input name="currentPassword" class="form-control" type="password" ng-model="password.current" required/>
<error-messages input="userPasswordForm.currentPassword" form="userPasswordForm"></error-messages>
</div>
<div class="form-group required" ng-class="{ 'has-error': (userPasswordForm.newPassword | showError:userPasswordForm )}">
<label class="control-label">New Password</label>
<input name="newPassword" class="form-control" type="password" ng-model="password.new" ng-minlength="6"
required/>
<error-messages input="userPasswordForm.newPassword" form="userPasswordForm"></error-messages>
</div>
<div class="form-group required" ng-class="{ 'has-error': (userPasswordForm.passwordRepeat | showError:userPasswordForm )}">
<label class="control-label">Repeat New Password</label>
<input name="passwordRepeat" class="form-control" type="password" ng-model="password.newRepeat"
compare-to="password.new"/>
<span class="help-block error"
ng-if="userPasswordForm.passwordRepeat.$error.compareTo">Passwords don't match.</span>
</div>
<div class="form-group">
<button class="btn btn-primary">Set new password</button>
</div>
</form>
</div>
</div>

<div ng-if="currentUser.isAdmin">
<br>
<div class="form-group">
<button class="btn btn-default" ng-if="!user.is_invitation_pending" ng-click="sendPasswordReset()" ng-disabled="disablePasswordResetButton">Send
Password Reset Email
Expand Down
Loading