From 8c2aedac4d18edb21c07f8a9ab4522185d8a489f Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 26 Jan 2019 13:48:42 -0200
Subject: [PATCH 01/35] Update DynamicForm export
---
client/app/components/dynamic-form/DynamicForm.jsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index 329e2493a7..2e47f3868b 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -12,7 +12,7 @@ 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),
@@ -202,11 +202,10 @@ export class DynamicForm extends React.Component {
);
}
-}
+});
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) => {
@@ -231,7 +230,7 @@ export default function init(ngModule) {
feedbackIcons: true,
onSubmit,
};
- return ( );
+ return ( );
}, ['target', 'type', 'actions']));
}
From 16361a4eabcaf5bc519cf078f69a93fa94ef136d Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 26 Jan 2019 13:49:12 -0200
Subject: [PATCH 02/35] Move UserShow to users folder
---
client/app/components/{ => users}/UserShow.jsx | 0
client/app/components/{ => users}/UserShow.test.js | 1 -
2 files changed, 1 deletion(-)
rename client/app/components/{ => users}/UserShow.jsx (100%)
rename client/app/components/{ => users}/UserShow.test.js (99%)
diff --git a/client/app/components/UserShow.jsx b/client/app/components/users/UserShow.jsx
similarity index 100%
rename from client/app/components/UserShow.jsx
rename to client/app/components/users/UserShow.jsx
diff --git a/client/app/components/UserShow.test.js b/client/app/components/users/UserShow.test.js
similarity index 99%
rename from client/app/components/UserShow.test.js
rename to client/app/components/users/UserShow.test.js
index e44f6b6481..59cbd4bfe6 100644
--- a/client/app/components/UserShow.test.js
+++ b/client/app/components/users/UserShow.test.js
@@ -7,4 +7,3 @@ test('renders correctly', () => {
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
-
From afdceff9b9ce7de2d45446be144ac879e268bb61 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 26 Jan 2019 13:58:43 -0200
Subject: [PATCH 03/35] Migrate User profile header and create DynamicForm for
basic data
---
client/app/components/proptypes.js | 7 ++++
client/app/components/users/UserEdit.jsx | 47 ++++++++++++++++++++++++
client/app/pages/users/show.html | 28 +-------------
client/app/pages/users/show.js | 9 +++++
4 files changed, 65 insertions(+), 26 deletions(-)
create mode 100644 client/app/components/users/UserEdit.jsx
diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js
index 19b7f6c5aa..c34ac01260 100644
--- a/client/app/components/proptypes.js
+++ b/client/app/components/proptypes.js
@@ -49,3 +49,10 @@ export const Action = PropTypes.shape({
export const AntdForm = PropTypes.shape({
validateFieldsAndScroll: PropTypes.func,
});
+
+export const UserProfile = PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ email: PropTypes.string,
+ profileImageUrl: PropTypes.string,
+});
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
new file mode 100644
index 0000000000..ce63ed637e
--- /dev/null
+++ b/client/app/components/users/UserEdit.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { react2angular } from 'react2angular';
+import { UserProfile } from '../proptypes';
+import { DynamicForm } from '../dynamic-form/DynamicForm';
+
+export const UserEdit = ({ user }) => {
+ 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 (
+
+
+
{user.name}
+
+
+
+ );
+};
+
+UserEdit.propTypes = {
+ user: UserProfile.isRequired,
+};
+
+export default function init(ngModule) {
+ ngModule.component('userEdit', react2angular(UserEdit));
+}
+
+init.init = true;
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index e2419b9579..4f99bcdaa8 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -4,33 +4,9 @@
-
-
-
-
-
{{user.name}}
-
-
-
-
+
+
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index b5bfbd39b5..bea97fba0e 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -41,11 +41,20 @@ function UserCtrl(
$scope.selectTab($location.hash() || 'profile');
+ $scope.userInfo = {};
+
$scope.user = User.get({ id: $scope.userId }, (user) => {
if (user.auth_type === 'password') {
$scope.showSettings = $scope.canEdit;
$scope.showPasswordSettings = $scope.canEdit;
}
+
+ $scope.userInfo = {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ profileImageUrl: user.profile_image_url,
+ };
});
$scope.password = {
From 8cfb157b241476fae0c6aadb19c08ab67f6329b8 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 26 Jan 2019 14:04:09 -0200
Subject: [PATCH 04/35] Update UserShow to use UserProfile prop
---
client/app/components/users/UserShow.jsx | 9 +++------
client/app/components/users/UserShow.test.js | 8 +++++++-
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/client/app/components/users/UserShow.jsx b/client/app/components/users/UserShow.jsx
index 1e306bfb25..610824508e 100644
--- a/client/app/components/users/UserShow.jsx
+++ b/client/app/components/users/UserShow.jsx
@@ -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 } }) => (
(
);
UserShow.propTypes = {
- name: PropTypes.string.isRequired,
- email: PropTypes.string.isRequired,
- profileImageUrl: PropTypes.string.isRequired,
+ user: UserProfile.isRequired,
};
export default function init(ngModule) {
diff --git a/client/app/components/users/UserShow.test.js b/client/app/components/users/UserShow.test.js
index 59cbd4bfe6..9170cb8def 100644
--- a/client/app/components/users/UserShow.test.js
+++ b/client/app/components/users/UserShow.test.js
@@ -3,7 +3,13 @@ import renderer from 'react-test-renderer';
import { UserShow } from './UserShow';
test('renders correctly', () => {
- const component = renderer.create(
);
+ const user = {
+ name: 'John Doe',
+ email: 'john@doe.com',
+ profileImageUrl: 'http://www.images.com/llama.jpg',
+ };
+
+ const component = renderer.create(
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
From c456fb1f94f82431e7dd2eb72a631799db981465 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 27 Jan 2019 16:00:32 -0200
Subject: [PATCH 05/35] Add API Key input
---
client/app/components/proptypes.js | 7 +-
client/app/components/users/UserEdit.jsx | 97 +++++++++++++++---------
client/app/pages/users/show.html | 4 +-
client/app/pages/users/show.js | 3 +-
4 files changed, 68 insertions(+), 43 deletions(-)
diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js
index c34ac01260..98bebf8ccb 100644
--- a/client/app/components/proptypes.js
+++ b/client/app/components/proptypes.js
@@ -51,8 +51,9 @@ export const AntdForm = PropTypes.shape({
});
export const UserProfile = PropTypes.shape({
- id: PropTypes.number,
- name: PropTypes.string,
- email: PropTypes.string,
+ id: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ email: PropTypes.string.isRequired,
profileImageUrl: PropTypes.string,
+ apiKey: PropTypes.string,
});
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index ce63ed637e..e21c02a751 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -1,44 +1,69 @@
import React from 'react';
+import Icon from 'antd/lib/icon';
+import Input from 'antd/lib/input';
+import Tooltip from 'antd/lib/tooltip';
import { react2angular } from 'react2angular';
import { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';
-export const UserEdit = ({ user }) => {
- 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 (
-
-
-
{user.name}
-
-
-
- );
-};
-
-UserEdit.propTypes = {
- user: UserProfile.isRequired,
-};
+export class UserEdit extends React.Component {
+ static propTypes = {
+ user: UserProfile.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = { apiKey: props.user.apiKey };
+ }
+
+ regenerateApiKey = () => {
+ // TODO
+ };
+
+ render() {
+ const { user } = this.props;
+
+ const formFields = [
+ {
+ name: 'name',
+ title: 'Name',
+ type: 'text',
+ initialValue: user.name,
+ required: true,
+ },
+ {
+ name: 'email',
+ title: 'Email',
+ type: 'email',
+ initialValue: user.email,
+ required: true,
+ },
+ ];
+
+ const regenerateButton = (
+
+
+
+ );
+
+ return (
+
+
+
{user.name}
+
+
+
+
API Key
+
+
+ );
+ }
+}
export default function init(ngModule) {
ngModule.component('userEdit', react2angular(UserEdit));
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index 4f99bcdaa8..f07c0bb802 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -3,8 +3,8 @@
-
-
+
+
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index bea97fba0e..4609aff272 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -41,8 +41,6 @@ function UserCtrl(
$scope.selectTab($location.hash() || 'profile');
- $scope.userInfo = {};
-
$scope.user = User.get({ id: $scope.userId }, (user) => {
if (user.auth_type === 'password') {
$scope.showSettings = $scope.canEdit;
@@ -54,6 +52,7 @@ function UserCtrl(
name: user.name,
email: user.email,
profileImageUrl: user.profile_image_url,
+ apiKey: user.api_key,
};
});
From d4fc964dcc35a3b44e1b03749989f9de3986ee50 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 27 Jan 2019 22:09:56 -0200
Subject: [PATCH 06/35] Add handler to regenerate API Key button
---
client/app/components/users/UserEdit.jsx | 18 +++++++++++++-
client/app/pages/users/show.html | 11 ---------
client/app/pages/users/show.js | 30 +-----------------------
client/app/services/user.js | 18 ++++++++++++++
4 files changed, 36 insertions(+), 41 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index e21c02a751..17416f58f0 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -2,7 +2,9 @@ import React from 'react';
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 { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';
@@ -17,7 +19,21 @@ export class UserEdit extends React.Component {
}
regenerateApiKey = () => {
- // TODO
+ const doRegenerate = () => {
+ User.regenerateApiKey(this.props.user).then(({ data }) => {
+ if (data) {
+ this.setState({ apiKey: data.api_key });
+ }
+ });
+ };
+
+ Modal.confirm({
+ title: 'Regenerate API Key',
+ content: 'Are you sure you want to regenerate?',
+ okText: 'Regenerate',
+ onOk: doRegenerate,
+ autoFocusButton: null,
+ });
};
render() {
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index f07c0bb802..acb941244b 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -14,17 +14,6 @@
Enable
-
-
- API Key
-
-
-
- Regenerate
-
-
-
-
Change password
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index 4609aff272..806102c1b3 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -6,7 +6,7 @@ import './settings.less';
function UserCtrl(
$scope, $routeParams, $http, $location, toastr,
- clientConfig, currentUser, User, AlertDialog,
+ clientConfig, currentUser, User,
) {
$scope.userId = $routeParams.userId;
$scope.currentUser = currentUser;
@@ -130,34 +130,6 @@ function UserCtrl(
$scope.disableUser = (user) => {
User.disableUser(user);
};
-
- $scope.regenerateUserApiKey = (user) => {
- const doRegenerate = () => {
- $scope.disableRegenerateApiKeyButton = true;
- $http
- .post(`api/users/${$scope.user.id}/regenerate_api_key`)
- .success((data) => {
- toastr.success('The API Key has been updated.');
- user.api_key = data.api_key;
- $scope.disableRegenerateApiKeyButton = false;
- })
- .error((response) => {
- const message =
- response.message
- ? response.message
- : `Failed regenerating API Key: ${response.statusText}`;
-
- toastr.error(message);
- $scope.disableRegenerateApiKeyButton = false;
- });
- };
-
- const title = 'Regenerate API Key';
- const message = 'Are you sure you want to regenerate?';
-
- AlertDialog.open(title, message, { class: 'btn-warning', title: 'Regenerate' })
- .then(doRegenerate);
- };
}
export default function init(ngModule) {
diff --git a/client/app/services/user.js b/client/app/services/user.js
index 9921b7adb8..11765ceb56 100644
--- a/client/app/services/user.js
+++ b/client/app/services/user.js
@@ -65,6 +65,23 @@ function deleteUser(user) {
});
}
+function regenerateApiKey(user) {
+ return $http
+ .post(`api/users/${user.id}/regenerate_api_key`)
+ .success((data) => {
+ toastr.success('The API Key has been updated.');
+ return data;
+ })
+ .error((response) => {
+ const message =
+ response.message
+ ? response.message
+ : `Failed regenerating API Key: ${response.statusText}`;
+
+ toastr.error(message);
+ });
+}
+
function UserService($resource) {
const actions = {
get: { method: 'GET' },
@@ -80,6 +97,7 @@ function UserService($resource) {
UserResource.enableUser = enableUser;
UserResource.disableUser = disableUser;
UserResource.deleteUser = deleteUser;
+ UserResource.regenerateApiKey = regenerateApiKey;
return UserResource;
}
From 2917e22ed338940ace7a887840dc09a99390bf69 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 27 Jan 2019 22:53:14 -0200
Subject: [PATCH 07/35] Handle user profile save
---
client/app/components/users/UserEdit.jsx | 35 ++++++++++++++++++++----
client/app/pages/users/show.js | 20 --------------
2 files changed, 29 insertions(+), 26 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 17416f58f0..0217ac9338 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -15,14 +15,37 @@ export class UserEdit extends React.Component {
constructor(props) {
super(props);
- this.state = { apiKey: props.user.apiKey };
+ this.state = { user: this.props.user };
}
+ handleSave = (values, onSuccess, onError) => {
+ const data = {
+ id: this.props.user.id,
+ ...values,
+ };
+
+ User.save(data, (user) => {
+ onSuccess('Saved.');
+ this.setState({
+ user: {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ profileImageUrl: user.profile_image_url,
+ apiKey: user.api_key,
+ },
+ });
+ }, (error) => {
+ onError(error.data.message || 'Failed saving.');
+ });
+ };
+
regenerateApiKey = () => {
const doRegenerate = () => {
- User.regenerateApiKey(this.props.user).then(({ data }) => {
+ User.regenerateApiKey(this.state.user).then(({ data }) => {
if (data) {
- this.setState({ apiKey: data.api_key });
+ const { user } = this.state;
+ this.setState({ user: { ...user, apiKey: data.api_key } });
}
});
};
@@ -37,7 +60,7 @@ export class UserEdit extends React.Component {
};
render() {
- const { user } = this.props;
+ const { user } = this.state;
const formFields = [
{
@@ -72,10 +95,10 @@ export class UserEdit extends React.Component {
/>
{user.name}
-
+
API Key
-
+
);
}
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index 806102c1b3..0dc3288fdd 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -86,26 +86,6 @@ function UserCtrl(
});
};
- $scope.updateUser = (form) => {
- if (!form.$valid) {
- return;
- }
-
- const data = {
- id: $scope.user.id,
- name: $scope.user.name,
- email: $scope.user.email,
- };
-
- User.save(data, (user) => {
- toastr.success('Saved.');
- $scope.user = user;
- }, (error) => {
- const message = error.data.message || 'Failed saving.';
- toastr.error(message);
- });
- };
-
$scope.isCollapsed = true;
$scope.sendPasswordReset = () => {
From fff4e7a277df6aadcd2e0fe5e6c0401ecfa1db07 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Mon, 28 Jan 2019 10:21:03 -0200
Subject: [PATCH 08/35] Add readOnly prop to DynamicForm and begin disabled
user behavior
---
.../components/dynamic-form/DynamicForm.jsx | 15 ++++++----
client/app/components/proptypes.js | 1 +
client/app/components/users/UserEdit.jsx | 29 +++++++++++++------
client/app/pages/users/show.js | 1 +
4 files changed, 31 insertions(+), 15 deletions(-)
diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index 2e47f3868b..f8c624f3e5 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -17,6 +17,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
fields: PropTypes.arrayOf(Field),
actions: PropTypes.arrayOf(Action),
feedbackIcons: PropTypes.bool,
+ readOnly: PropTypes.bool,
onSubmit: PropTypes.func,
form: AntdForm.isRequired,
};
@@ -25,6 +26,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
fields: [],
actions: [],
feedbackIcons: false,
+ readOnly: false,
onSubmit: () => {},
};
@@ -139,23 +141,25 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
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,
};
@@ -191,13 +195,12 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
disabled: this.state.isSubmitting,
loading: this.state.isSubmitting,
};
+ const saveButton = !this.props.readOnly;
return (
);
diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js
index 98bebf8ccb..7d14c3e3f3 100644
--- a/client/app/components/proptypes.js
+++ b/client/app/components/proptypes.js
@@ -56,4 +56,5 @@ export const UserProfile = PropTypes.shape({
email: PropTypes.string.isRequired,
profileImageUrl: PropTypes.string,
apiKey: PropTypes.string,
+ isDisabled: PropTypes.bool,
});
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 0217ac9338..965174f3b9 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -33,6 +33,7 @@ export class UserEdit extends React.Component {
email: user.email,
profileImageUrl: user.profile_image_url,
apiKey: user.api_key,
+ isDisabled: user.is_disabled,
},
});
}, (error) => {
@@ -59,6 +60,23 @@ export class UserEdit extends React.Component {
});
};
+ renderApiKey() {
+ const { user } = this.state;
+
+ const regenerateButton = (
+
+
+
+ );
+
+ return (
+
+ API Key
+
+
+ );
+ }
+
render() {
const { user } = this.state;
@@ -79,12 +97,6 @@ export class UserEdit extends React.Component {
},
];
- const regenerateButton = (
-
-
-
- );
-
return (
{user.name}
-
+
-
API Key
-
+ {!user.isDisabled && this.renderApiKey()}
);
}
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index 0dc3288fdd..71c526de9f 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -53,6 +53,7 @@ function UserCtrl(
email: user.email,
profileImageUrl: user.profile_image_url,
apiKey: user.api_key,
+ isDisabled: user.is_disabled,
};
});
From d17266ec1712e81c861b5bf2df7f18a2d85c8b37 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Mon, 28 Jan 2019 23:42:10 -0200
Subject: [PATCH 09/35] Add Change Password Modal
---
.../components/dynamic-form/DynamicForm.jsx | 7 ++--
client/app/components/users/UserEdit.jsx | 35 +++++++++++++++++--
client/app/pages/users/show.html | 31 ----------------
client/app/pages/users/show.js | 32 -----------------
4 files changed, 38 insertions(+), 67 deletions(-)
diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index f8c624f3e5..f3d2eae656 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -18,6 +18,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
actions: PropTypes.arrayOf(Action),
feedbackIcons: PropTypes.bool,
readOnly: PropTypes.bool,
+ saveText: PropTypes.string,
onSubmit: PropTypes.func,
form: AntdForm.isRequired,
};
@@ -27,6 +28,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
actions: [],
feedbackIcons: false,
readOnly: false,
+ saveText: 'Save',
onSubmit: () => {},
};
@@ -195,12 +197,13 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
disabled: this.state.isSubmitting,
loading: this.state.isSubmitting,
};
- const saveButton = !this.props.readOnly;
+ const { readOnly, saveText } = this.props;
+ const saveButton = !readOnly;
return (
);
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 965174f3b9..4077c6735e 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -1,10 +1,12 @@
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';
@@ -15,7 +17,7 @@ export class UserEdit extends React.Component {
constructor(props) {
super(props);
- this.state = { user: this.props.user };
+ this.state = { user: this.props.user, changePassword: false };
}
handleSave = (values, onSuccess, onError) => {
@@ -41,6 +43,30 @@ export class UserEdit extends React.Component {
});
};
+ 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 (
+ { this.setState({ changePassword: false }); }}
+ footer={null}
+ destroyOnClose
+ >
+
+
+ );
+ }
+
regenerateApiKey = () => {
const doRegenerate = () => {
User.regenerateApiKey(this.state.user).then(({ data }) => {
@@ -56,6 +82,7 @@ export class UserEdit extends React.Component {
content: 'Are you sure you want to regenerate?',
okText: 'Regenerate',
onOk: doRegenerate,
+ maskClosable: true,
autoFocusButton: null,
});
};
@@ -71,6 +98,7 @@ export class UserEdit extends React.Component {
return (
+
API Key
@@ -108,8 +136,11 @@ export class UserEdit extends React.Component {
{user.name}
-
+ {this.changePasswordModal()}
{!user.isDisabled && this.renderApiKey()}
+
+ Change Password
+ {currentUser.isAdmin && Send Password Reset Email }
);
}
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index acb941244b..b64db680e7 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -14,38 +14,7 @@
Enable
-
Change password
-
-
-
-
Send
Password Reset Email
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index 71c526de9f..e2014c4aa4 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -57,38 +57,6 @@ function UserCtrl(
};
});
- $scope.password = {
- current: '',
- new: '',
- newRepeat: '',
- };
-
- $scope.savePassword = (form) => {
- if (!form.$valid) {
- return;
- }
-
- const data = {
- id: $scope.user.id,
- password: $scope.password.new,
- old_password: $scope.password.current,
- };
-
- User.save(data, () => {
- toastr.success('Password Saved.');
- $scope.password = {
- current: '',
- new: '',
- newRepeat: '',
- };
- }, (error) => {
- const message = error.data.message || 'Failed saving password.';
- toastr.error(message);
- });
- };
-
- $scope.isCollapsed = true;
-
$scope.sendPasswordReset = () => {
$scope.disablePasswordResetButton = true;
$http.post(`api/users/${$scope.user.id}/reset_password`).success((data) => {
From 576c3d0c36f5d0979b29a8707697fab981f5c3ae Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Tue, 29 Jan 2019 01:48:18 -0200
Subject: [PATCH 10/35] Remove action buttons for disabled users
---
client/app/components/users/UserEdit.jsx | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 4077c6735e..edcf9ff4d6 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -137,10 +137,14 @@ export class UserEdit extends React.Component {
{this.changePasswordModal()}
- {!user.isDisabled && this.renderApiKey()}
-
- Change Password
- {currentUser.isAdmin && Send Password Reset Email }
+ {!user.isDisabled && (
+
+ {this.renderApiKey()}
+
+ Change Password
+ {currentUser.isAdmin && Send Password Reset Email }
+
+ )}
);
}
From d891df1ad103d9a0171f20f9a644d42133f7aba9 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Wed, 30 Jan 2019 22:54:30 -0200
Subject: [PATCH 11/35] Add send password reset behavior
---
client/app/assets/less/ant.less | 1 +
.../components/dynamic-form/DynamicForm.jsx | 2 +-
client/app/components/users/UserEdit.jsx | 70 +++++++++++++++----
client/app/pages/users/show.html | 16 -----
client/app/pages/users/show.js | 12 +---
client/app/services/user.js | 23 ++++--
6 files changed, 80 insertions(+), 44 deletions(-)
diff --git a/client/app/assets/less/ant.less b/client/app/assets/less/ant.less
index c9985fa36d..4673f19e7e 100644
--- a/client/app/assets/less/ant.less
+++ b/client/app/assets/less/ant.less
@@ -1,5 +1,6 @@
@import '~antd/lib/style/core/iconfont';
@import '~antd/lib/style/core/motion';
+@import '~antd/lib/alert/style/index';
@import '~antd/lib/input/style/index';
@import '~antd/lib/input-number/style/index';
@import '~antd/lib/date-picker/style/index';
diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index f3d2eae656..558db28630 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -180,7 +180,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
htmlType: 'button',
className: action.pullRight ? 'pull-right m-t-10' : 'm-t-10',
type: action.type,
- disabled: inProgress || (isFieldsTouched() && action.disableWhenDirty),
+ disabled: (isFieldsTouched() && action.disableWhenDirty),
loading: inProgress,
onClick: this.handleAction,
};
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index edcf9ff4d6..9192bc4170 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Input from 'antd/lib/input';
@@ -6,7 +7,8 @@ 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 { currentUser, clientConfig } from '@/services/auth';
+import { absoluteUrl } from '@/services/utils';
import { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';
@@ -17,7 +19,11 @@ export class UserEdit extends React.Component {
constructor(props) {
super(props);
- this.state = { user: this.props.user, changePassword: false };
+ this.state = {
+ user: this.props.user,
+ changingPassword: false,
+ sendingPasswordEmail: false,
+ };
}
handleSave = (values, onSuccess, onError) => {
@@ -44,7 +50,18 @@ export class UserEdit extends React.Component {
};
openChangePasswordModal = () => {
- this.setState({ changePassword: true });
+ this.setState({ changingPassword: true });
+ };
+
+ sendPasswordReset = () => {
+ const { user } = this.state;
+ this.setState({ sendingPasswordEmail: true });
+
+ User.sendPasswordReset(user).then((passwordResetLink) => {
+ this.setState({ passwordResetLink });
+ }).finally(() => {
+ this.setState({ sendingPasswordEmail: false });
+ });
};
changePasswordModal() {
@@ -56,9 +73,9 @@ export class UserEdit extends React.Component {
return (
{ this.setState({ changePassword: false }); }}
+ onCancel={() => { this.setState({ changingPassword: false }); }}
footer={null}
destroyOnClose
>
@@ -69,11 +86,9 @@ export class UserEdit extends React.Component {
regenerateApiKey = () => {
const doRegenerate = () => {
- User.regenerateApiKey(this.state.user).then(({ data }) => {
- if (data) {
- const { user } = this.state;
- this.setState({ user: { ...user, apiKey: data.api_key } });
- }
+ User.regenerateApiKey(this.state.user).then((apiKey) => {
+ const { user } = this.state;
+ this.setState({ user: { ...user, apiKey } });
});
};
@@ -105,6 +120,37 @@ export class UserEdit extends React.Component {
);
}
+ renderPasswordReset() {
+ const { user, sendingPasswordEmail, passwordResetLink } = this.state;
+
+ return (
+
+
+ Send Password Reset Email
+
+ {passwordResetLink &&
+
+ The mail server is not configured, please send the following link
+ to {user.name} to reset their password:
+
+
+ ) : 'The user should receive a link to reset their password by email soon.'}
+ type="success"
+ className="m-t-20"
+ afterClose={() => { this.setState({ passwordResetLink: null }); }}
+ closable
+ />
+ }
+
+ );
+ }
+
render() {
const { user } = this.state;
@@ -136,13 +182,13 @@ export class UserEdit extends React.Component {
{user.name}
- {this.changePasswordModal()}
{!user.isDisabled && (
{this.renderApiKey()}
+ {this.changePasswordModal()}
Change Password
- {currentUser.isAdmin && Send Password Reset Email }
+ {currentUser.isAdmin && this.renderPasswordReset()}
)}
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index b64db680e7..85223c801c 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -7,8 +7,6 @@
-
-
This user is disabled.
Enable
@@ -16,22 +14,8 @@
- Send
- Password Reset Email
-
Resend Invitation
-
-
-
- The user should receive a link to reset their password by email soon.
-
-
- You don't have mail server configured, please send the following link
- to {{user.name}} to reset their password:
-
-
-
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index e2014c4aa4..538c1a2031 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -1,16 +1,14 @@
import { each } from 'lodash';
import settingsMenu from '@/services/settingsMenu';
-import { absoluteUrl } from '@/services/utils';
import template from './show.html';
import './settings.less';
function UserCtrl(
$scope, $routeParams, $http, $location, toastr,
- clientConfig, currentUser, User,
+ currentUser, User,
) {
$scope.userId = $routeParams.userId;
$scope.currentUser = currentUser;
- $scope.clientConfig = clientConfig;
if ($scope.userId === undefined) {
$scope.userId = currentUser.id;
@@ -57,14 +55,6 @@ function UserCtrl(
};
});
- $scope.sendPasswordReset = () => {
- $scope.disablePasswordResetButton = true;
- $http.post(`api/users/${$scope.user.id}/reset_password`).success((data) => {
- $scope.disablePasswordResetButton = false;
- $scope.passwordResetLink = absoluteUrl(data.reset_link);
- });
- };
-
$scope.resendInvitation = () => {
$http.post(`api/users/${$scope.user.id}/invite`).success(() => {
toastr.success('Invitation sent.', {
diff --git a/client/app/services/user.js b/client/app/services/user.js
index 11765ceb56..0c46877e9b 100644
--- a/client/app/services/user.js
+++ b/client/app/services/user.js
@@ -68,15 +68,29 @@ function deleteUser(user) {
function regenerateApiKey(user) {
return $http
.post(`api/users/${user.id}/regenerate_api_key`)
- .success((data) => {
+ .then(({ data }) => {
toastr.success('The API Key has been updated.');
- return data;
+ return data.api_key;
})
- .error((response) => {
+ .catch((response) => {
+ const message =
+ response.data && response.data.message
+ ? response.data.message
+ : `Failed regenerating API Key: ${response.statusText}`;
+
+ toastr.error(message);
+ });
+}
+
+function sendPasswordReset(user) {
+ return $http
+ .post(`api/users/${user.id}/reset_password`)
+ .then(({ data }) => data.reset_link)
+ .catch((response) => {
const message =
response.message
? response.message
- : `Failed regenerating API Key: ${response.statusText}`;
+ : `Failed to send password reset email: ${response.statusText}`;
toastr.error(message);
});
@@ -98,6 +112,7 @@ function UserService($resource) {
UserResource.disableUser = disableUser;
UserResource.deleteUser = deleteUser;
UserResource.regenerateApiKey = regenerateApiKey;
+ UserResource.sendPasswordReset = sendPasswordReset;
return UserResource;
}
From fb1f5714381cace31aefa66e6a3b5f9efac7ec57 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 2 Feb 2019 16:38:54 -0200
Subject: [PATCH 12/35] Add minLength and password comparison to Password Modal
---
.../components/dynamic-form/DynamicForm.jsx | 19 +++--
client/app/components/proptypes.js | 1 +
client/app/components/users/UserEdit.jsx | 72 +++++++++++--------
3 files changed, 57 insertions(+), 35 deletions(-)
diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index 2aacd45f6f..6c075894b5 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -12,6 +12,18 @@ import { toastr } from '@/services/ng';
import { Field, Action, AntdForm } from '../proptypes';
import helper from './dynamicFormHelper';
+const fieldRules = ({ title, name, type, required, minLength }) => {
+ const fieldLabel = title || helper.toHuman(name);
+
+ const requiredRule = required;
+ const minLengthRule = minLength && ['text', 'email', 'password'].includes(type);
+
+ return [
+ requiredRule && { required, message: `${fieldLabel} is required.` },
+ minLengthRule && { min: minLength, message: `${fieldLabel} is too short.` },
+ ].filter(rule => rule);
+};
+
export const DynamicForm = Form.create()(class DynamicForm extends React.Component {
static propTypes = {
fields: PropTypes.arrayOf(Field),
@@ -100,11 +112,10 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
renderUpload(field, props) {
const { getFieldDecorator, getFieldValue } = this.props.form;
- const { name, initialValue, required } = field;
- const fieldLabel = field.title || helper.toHuman(name);
+ const { name, initialValue } = field;
const fileOptions = {
- rules: [{ required, message: `${fieldLabel} is required.` }],
+ rules: fieldRules(field),
initialValue,
getValueFromEvent: this.base64File.bind(this, name),
};
@@ -126,7 +137,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
const fieldLabel = field.title || helper.toHuman(name);
const options = {
- rules: [{ required: field.required, message: `${fieldLabel} is required.` }],
+ rules: fieldRules(field),
valuePropName: type === 'checkbox' ? 'checked' : 'value',
initialValue,
};
diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js
index 7d14c3e3f3..83b0df853c 100644
--- a/client/app/components/proptypes.js
+++ b/client/app/components/proptypes.js
@@ -35,6 +35,7 @@ export const Field = PropTypes.shape({
type: PropTypes.oneOf(['text', 'email', 'password', 'number', 'checkbox', 'file']).isRequired,
initialValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
required: PropTypes.bool,
+ minLength: PropTypes.number,
placeholder: PropTypes.string,
});
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 9192bc4170..c038ea77dc 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -1,6 +1,7 @@
-import React from 'react';
+import React, { Fragment } from 'react';
import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button';
+import Form from 'antd/lib/form';
import Icon from 'antd/lib/icon';
import Input from 'antd/lib/input';
import Tooltip from 'antd/lib/tooltip';
@@ -49,6 +50,14 @@ export class UserEdit extends React.Component {
});
};
+ handleUpdatePassword = (values, onSuccess, onError) => {
+ if (values.password === values.password_repeat) {
+ this.handleSave(values, onSuccess, onError);
+ } else {
+ onError('Passwords don\'t match!');
+ }
+ }
+
openChangePasswordModal = () => {
this.setState({ changingPassword: true });
};
@@ -64,26 +73,6 @@ export class UserEdit extends React.Component {
});
};
- 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 (
- { this.setState({ changingPassword: false }); }}
- footer={null}
- destroyOnClose
- >
-
-
- );
- }
-
regenerateApiKey = () => {
const doRegenerate = () => {
User.regenerateApiKey(this.state.user).then((apiKey) => {
@@ -102,6 +91,26 @@ export class UserEdit extends React.Component {
});
};
+ changePasswordModal() {
+ const fields = [
+ { name: 'old_password', title: 'Current Password' },
+ { name: 'password', title: 'New Password', minLength: 6 },
+ { name: 'password_repeat', title: 'Repeat New Password' },
+ ].map(field => ({ ...field, type: 'password', required: true }));
+
+ return (
+ { this.setState({ changingPassword: false }); }}
+ footer={null}
+ destroyOnClose
+ >
+
+
+ );
+ }
+
renderApiKey() {
const { user } = this.state;
@@ -112,11 +121,12 @@ export class UserEdit extends React.Component {
);
return (
-
+
+
+
+
+
);
}
@@ -124,7 +134,7 @@ export class UserEdit extends React.Component {
const { user, sendingPasswordEmail, passwordResetLink } = this.state;
return (
-
+
Send Password Reset Email
- {passwordResetLink &&
+ {passwordResetLink && (
@@ -146,8 +156,8 @@ export class UserEdit extends React.Component {
afterClose={() => { this.setState({ passwordResetLink: null }); }}
closable
/>
- }
-
+ )}
+
);
}
@@ -183,13 +193,13 @@ export class UserEdit extends React.Component {
{!user.isDisabled && (
-
+
{this.renderApiKey()}
{this.changePasswordModal()}
Change Password
{currentUser.isAdmin && this.renderPasswordReset()}
-
+
)}
);
From 7c6f11e53f86ef03e763d37dbb38e68b4773b9da Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 2 Feb 2019 17:59:01 -0200
Subject: [PATCH 13/35] Resend Invitation button
---
client/app/components/users/UserEdit.jsx | 43 ++++++++++++++++++------
client/app/pages/users/show.html | 7 ----
client/app/pages/users/show.js | 9 +----
client/app/services/user.js | 17 ++++++++++
4 files changed, 50 insertions(+), 26 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index c038ea77dc..b3063ebc74 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -24,6 +24,7 @@ export class UserEdit extends React.Component {
user: this.props.user,
changingPassword: false,
sendingPasswordEmail: false,
+ resendingInvitation: false,
};
}
@@ -43,6 +44,7 @@ export class UserEdit extends React.Component {
profileImageUrl: user.profile_image_url,
apiKey: user.api_key,
isDisabled: user.is_disabled,
+ isInvitationPending: user.is_invitation_pending,
},
});
}, (error) => {
@@ -73,6 +75,15 @@ export class UserEdit extends React.Component {
});
};
+ resendInvitation = () => {
+ const { user } = this.state;
+ this.setState({ resendingInvitation: true });
+
+ User.resendInvitation(user).finally(() => {
+ this.setState({ resendingInvitation: false });
+ });
+ };
+
regenerateApiKey = () => {
const doRegenerate = () => {
User.regenerateApiKey(this.state.user).then((apiKey) => {
@@ -130,24 +141,34 @@ export class UserEdit extends React.Component {
);
}
- renderPasswordReset() {
- const { user, sendingPasswordEmail, passwordResetLink } = this.state;
+ renderPasswordOptions() {
+ const { sendingPasswordEmail, passwordResetLink, resendingInvitation } = this.state;
return (
-
- Send Password Reset Email
-
+ {this.state.user.isInvitationPending ? (
+
+ Resend Invitation
+
+ ) : (
+
+ Send Password Reset Email
+
+ )}
{passwordResetLink && (
The mail server is not configured, please send the following link
- to {user.name} to reset their password:
+ to {this.state.user.name} to reset their password:
) : 'The user should receive a link to reset their password by email soon.'}
@@ -198,7 +219,7 @@ export class UserEdit extends React.Component {
{this.changePasswordModal()}
Change Password
- {currentUser.isAdmin && this.renderPasswordReset()}
+ {currentUser.isAdmin && this.renderPasswordOptions()}
)}
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index 85223c801c..a807867bc0 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -11,13 +11,6 @@
This user is disabled.
Enable
-
-
-
- Resend Invitation
-
-
-
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index f5e37e1c09..072cfb32db 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -52,17 +52,10 @@ function UserCtrl(
profileImageUrl: user.profile_image_url,
apiKey: user.api_key,
isDisabled: user.is_disabled,
+ isInvitationPending: user.is_invitation_pending,
};
});
- $scope.resendInvitation = () => {
- $http.post(`api/users/${$scope.user.id}/invite`).success(() => {
- toastr.success('Invitation sent.', {
- timeOut: 10000,
- });
- });
- };
-
$scope.enableUser = (user) => {
User.enableUser(user);
};
diff --git a/client/app/services/user.js b/client/app/services/user.js
index 201b568b2b..91e64817c4 100644
--- a/client/app/services/user.js
+++ b/client/app/services/user.js
@@ -96,6 +96,22 @@ function sendPasswordReset(user) {
});
}
+function resendInvitation(user) {
+ return $http
+ .post(`api/users/${user.id}/invite`)
+ .then(() => {
+ toastr.success('Invitation sent.');
+ })
+ .catch((response) => {
+ const message =
+ response.message
+ ? response.message
+ : `Failed to resend invitation: ${response.statusText}`;
+
+ toastr.error(message);
+ });
+}
+
function UserService($resource) {
const actions = {
get: { method: 'GET' },
@@ -113,6 +129,7 @@ function UserService($resource) {
UserResource.deleteUser = deleteUser;
UserResource.regenerateApiKey = regenerateApiKey;
UserResource.sendPasswordReset = sendPasswordReset;
+ UserResource.resendInvitation = resendInvitation;
return UserResource;
}
From 82237e7e55769d9e96acb0d6a920f704a8b8131b Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 3 Feb 2019 02:06:40 -0200
Subject: [PATCH 14/35] Add Convert User Info
---
client/app/components/users/UserEdit.jsx | 10 +---------
client/app/pages/users/show.js | 10 +---------
client/app/services/user.js | 13 +++++++++++++
3 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index b3063ebc74..b3cb3f8df3 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -37,15 +37,7 @@ export class UserEdit extends React.Component {
User.save(data, (user) => {
onSuccess('Saved.');
this.setState({
- user: {
- id: user.id,
- name: user.name,
- email: user.email,
- profileImageUrl: user.profile_image_url,
- apiKey: user.api_key,
- isDisabled: user.is_disabled,
- isInvitationPending: user.is_invitation_pending,
- },
+ user: User.convertUserInfo(user),
});
}, (error) => {
onError(error.data.message || 'Failed saving.');
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index 072cfb32db..1e90b515e6 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -45,15 +45,7 @@ function UserCtrl(
$scope.showPasswordSettings = $scope.canEdit;
}
- $scope.userInfo = {
- id: user.id,
- name: user.name,
- email: user.email,
- profileImageUrl: user.profile_image_url,
- apiKey: user.api_key,
- isDisabled: user.is_disabled,
- isInvitationPending: user.is_invitation_pending,
- };
+ $scope.userInfo = User.convertUserInfo(user);
});
$scope.enableUser = (user) => {
diff --git a/client/app/services/user.js b/client/app/services/user.js
index 91e64817c4..e6d44e6b26 100644
--- a/client/app/services/user.js
+++ b/client/app/services/user.js
@@ -65,6 +65,18 @@ function deleteUser(user) {
});
}
+function convertUserInfo(user) {
+ return {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ profileImageUrl: user.profile_image_url,
+ apiKey: user.api_key,
+ isDisabled: user.is_disabled,
+ isInvitationPending: user.is_invitation_pending,
+ };
+}
+
function regenerateApiKey(user) {
return $http
.post(`api/users/${user.id}/regenerate_api_key`)
@@ -127,6 +139,7 @@ function UserService($resource) {
UserResource.enableUser = enableUser;
UserResource.disableUser = disableUser;
UserResource.deleteUser = deleteUser;
+ UserResource.convertUserInfo = convertUserInfo;
UserResource.regenerateApiKey = regenerateApiKey;
UserResource.sendPasswordReset = sendPasswordReset;
UserResource.resendInvitation = resendInvitation;
From f85668bdcc12f21d7f3538684959ff6ed04e3e5b Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 3 Feb 2019 12:19:44 -0200
Subject: [PATCH 15/35] Fix UserShow test
---
client/app/components/users/UserShow.test.js | 1 +
.../components/{ => users}/__snapshots__/UserShow.test.js.snap | 0
2 files changed, 1 insertion(+)
rename client/app/components/{ => users}/__snapshots__/UserShow.test.js.snap (100%)
diff --git a/client/app/components/users/UserShow.test.js b/client/app/components/users/UserShow.test.js
index 9170cb8def..f2ad155514 100644
--- a/client/app/components/users/UserShow.test.js
+++ b/client/app/components/users/UserShow.test.js
@@ -4,6 +4,7 @@ import { UserShow } from './UserShow';
test('renders correctly', () => {
const user = {
+ id: 2,
name: 'John Doe',
email: 'john@doe.com',
profileImageUrl: 'http://www.images.com/llama.jpg',
diff --git a/client/app/components/__snapshots__/UserShow.test.js.snap b/client/app/components/users/__snapshots__/UserShow.test.js.snap
similarity index 100%
rename from client/app/components/__snapshots__/UserShow.test.js.snap
rename to client/app/components/users/__snapshots__/UserShow.test.js.snap
From f5c4090cb5f4d281c7e8d7f3ddd3e5d2d29083b0 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 3 Feb 2019 14:30:54 -0200
Subject: [PATCH 16/35] Some code updates
---
client/app/components/users/UserEdit.jsx | 81 ++++++++++++------------
1 file changed, 42 insertions(+), 39 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index b3cb3f8df3..9a2cd36b3d 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -28,35 +28,33 @@ export class UserEdit extends React.Component {
};
}
- handleSave = (values, onSuccess, onError) => {
+ onSaveUser = (values, successCallback, errorCallback) => {
const data = {
id: this.props.user.id,
...values,
};
User.save(data, (user) => {
- onSuccess('Saved.');
- this.setState({
- user: User.convertUserInfo(user),
- });
+ successCallback('Saved.');
+ this.setState({ user: User.convertUserInfo(user) });
}, (error) => {
- onError(error.data.message || 'Failed saving.');
+ errorCallback(error.data.message || 'Failed saving.');
});
};
- handleUpdatePassword = (values, onSuccess, onError) => {
+ onUpdatePassword = (values, successCallback, errorCallback) => {
if (values.password === values.password_repeat) {
- this.handleSave(values, onSuccess, onError);
+ this.onSaveUser(values, successCallback, errorCallback);
} else {
- onError('Passwords don\'t match!');
+ errorCallback('Passwords don\'t match!');
}
}
- openChangePasswordModal = () => {
+ onClickChangePassword = () => {
this.setState({ changingPassword: true });
};
- sendPasswordReset = () => {
+ onClickSendPasswordReset = () => {
const { user } = this.state;
this.setState({ sendingPasswordEmail: true });
@@ -67,7 +65,7 @@ export class UserEdit extends React.Component {
});
};
- resendInvitation = () => {
+ onClickResendInvitation = () => {
const { user } = this.state;
this.setState({ resendingInvitation: true });
@@ -76,11 +74,10 @@ export class UserEdit extends React.Component {
});
};
- regenerateApiKey = () => {
+ onClickRegenerateApiKey = () => {
const doRegenerate = () => {
User.regenerateApiKey(this.state.user).then((apiKey) => {
- const { user } = this.state;
- this.setState({ user: { ...user, apiKey } });
+ this.setState(prevState => ({ user: { ...prevState.user, apiKey } }));
});
};
@@ -94,7 +91,7 @@ export class UserEdit extends React.Component {
});
};
- changePasswordModal() {
+ renderChangePasswordModal() {
const fields = [
{ name: 'old_password', title: 'Current Password' },
{ name: 'password', title: 'New Password', minLength: 6 },
@@ -109,7 +106,7 @@ export class UserEdit extends React.Component {
footer={null}
destroyOnClose
>
-
+
);
}
@@ -119,7 +116,7 @@ export class UserEdit extends React.Component {
const regenerateButton = (
-
+
);
@@ -133,6 +130,26 @@ export class UserEdit extends React.Component {
);
}
+ renderPasswordLinkAlert() {
+ const { user, passwordResetLink } = this.state;
+
+ return (
+
+ The mail server is not configured, please send the following link
+ to {user.name} to reset their password:
+
+
+ ) : 'The user should receive a link to reset their password by email soon.'}
+ type="success"
+ className="m-t-20"
+ afterClose={() => { this.setState({ passwordResetLink: null }); }}
+ closable
+ />
+ );
+ }
+
renderPasswordOptions() {
const { sendingPasswordEmail, passwordResetLink, resendingInvitation } = this.state;
@@ -141,7 +158,7 @@ export class UserEdit extends React.Component {
{this.state.user.isInvitationPending ? (
Resend Invitation
@@ -149,27 +166,13 @@ export class UserEdit extends React.Component {
) : (
Send Password Reset Email
)}
- {passwordResetLink && (
-
- The mail server is not configured, please send the following link
- to {this.state.user.name} to reset their password:
-
-
- ) : 'The user should receive a link to reset their password by email soon.'}
- type="success"
- className="m-t-20"
- afterClose={() => { this.setState({ passwordResetLink: null }); }}
- closable
- />
- )}
+ {passwordResetLink && this.renderPasswordLinkAlert()}
);
}
@@ -197,20 +200,20 @@ export class UserEdit extends React.Component {
return (
{user.name}
-
+
{!user.isDisabled && (
{this.renderApiKey()}
- {this.changePasswordModal()}
- Change Password
+ {this.renderChangePasswordModal()}
+ Change Password
{currentUser.isAdmin && this.renderPasswordOptions()}
)}
From 6cf8b2dfacddba78d570aec870869a138283ad6a Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 3 Feb 2019 15:19:43 -0200
Subject: [PATCH 17/35] Add enable/disable user button
---
client/app/components/users/UserEdit.jsx | 33 +++++++++++++++++++++---
client/app/pages/users/show.html | 7 -----
client/app/pages/users/show.js | 16 ------------
3 files changed, 29 insertions(+), 27 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 9a2cd36b3d..7c74babc0d 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -55,10 +55,9 @@ export class UserEdit extends React.Component {
};
onClickSendPasswordReset = () => {
- const { user } = this.state;
this.setState({ sendingPasswordEmail: true });
- User.sendPasswordReset(user).then((passwordResetLink) => {
+ User.sendPasswordReset(this.state.user).then((passwordResetLink) => {
this.setState({ passwordResetLink });
}).finally(() => {
this.setState({ sendingPasswordEmail: false });
@@ -66,10 +65,9 @@ export class UserEdit extends React.Component {
};
onClickResendInvitation = () => {
- const { user } = this.state;
this.setState({ resendingInvitation: true });
- User.resendInvitation(user).finally(() => {
+ User.resendInvitation(this.state.user).finally(() => {
this.setState({ resendingInvitation: false });
});
};
@@ -91,6 +89,18 @@ export class UserEdit extends React.Component {
});
};
+ onClickToggleUser = () => {
+ const { user } = this.state;
+ const toggleUser = user.isDisabled ? User.enableUser : User.disableUser;
+
+ this.setState({ togglingUser: true });
+ toggleUser(user).then(({ data }) => {
+ this.setState({ user: User.convertUserInfo(data) });
+ }).finally(() => {
+ this.setState({ togglingUser: false });
+ });
+ };
+
renderChangePasswordModal() {
const fields = [
{ name: 'old_password', title: 'Current Password' },
@@ -177,6 +187,20 @@ export class UserEdit extends React.Component {
);
}
+ renderToggleUser() {
+ const { user, togglingUser } = this.state;
+
+ return user.isDisabled ? (
+
+ Enable User
+
+ ) : (
+
+ Disable User
+
+ );
+ }
+
render() {
const { user } = this.state;
@@ -217,6 +241,7 @@ export class UserEdit extends React.Component {
{currentUser.isAdmin && this.renderPasswordOptions()}
)}
+ {currentUser.isAdmin && this.renderToggleUser()}
);
}
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index a807867bc0..5c5d2b9ad2 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -5,13 +5,6 @@
-
-
-
- This user is disabled.
- Enable
-
-
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index 1e90b515e6..bb5fff9d96 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -14,10 +14,6 @@ function UserCtrl(
$scope.userId = currentUser.id;
}
- $scope.canEdit = currentUser.hasPermission('admin') || currentUser.id === parseInt($scope.userId, 10);
- $scope.showSettings = false;
- $scope.showPasswordSettings = false;
-
$scope.selectTab = (tab) => {
$scope.selectedTab = tab;
each($scope.tabs, (v, k) => {
@@ -40,20 +36,8 @@ function UserCtrl(
$scope.selectTab($location.hash() || 'profile');
$scope.user = User.get({ id: $scope.userId }, (user) => {
- if (user.auth_type === 'password') {
- $scope.showSettings = $scope.canEdit;
- $scope.showPasswordSettings = $scope.canEdit;
- }
-
$scope.userInfo = User.convertUserInfo(user);
});
-
- $scope.enableUser = (user) => {
- User.enableUser(user);
- };
- $scope.disableUser = (user) => {
- User.disableUser(user);
- };
}
export default function init(ngModule) {
From 8b7336bdf68646c54e47269baa2d16748b0995e7 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 3 Feb 2019 16:39:14 -0200
Subject: [PATCH 18/35] Add UserPolicy as an idea
---
client/app/components/users/UserEdit.jsx | 129 +++++++++++++----------
client/app/services/policy/UserPolicy.js | 33 ++++++
client/app/services/policy/index.js | 2 +
3 files changed, 106 insertions(+), 58 deletions(-)
create mode 100644 client/app/services/policy/UserPolicy.js
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 7c74babc0d..d2fd4008ca 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -8,7 +8,8 @@ import Tooltip from 'antd/lib/tooltip';
import Modal from 'antd/lib/modal';
import { react2angular } from 'react2angular';
import { User } from '@/services/user';
-import { currentUser, clientConfig } from '@/services/auth';
+import { userPolicy } from '@/services/policy';
+import { clientConfig } from '@/services/auth';
import { absoluteUrl } from '@/services/utils';
import { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';
@@ -101,7 +102,35 @@ export class UserEdit extends React.Component {
});
};
- renderChangePasswordModal() {
+ renderBasicInfoForm() {
+ 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 (
+
+ );
+ }
+
+ renderChangePassword() {
const fields = [
{ name: 'old_password', title: 'Current Password' },
{ name: 'password', title: 'New Password', minLength: 6 },
@@ -109,15 +138,19 @@ export class UserEdit extends React.Component {
].map(field => ({ ...field, type: 'password', required: true }));
return (
- { this.setState({ changingPassword: false }); }}
- footer={null}
- destroyOnClose
- >
-
-
+
+
+ { this.setState({ changingPassword: false }); }}
+ footer={null}
+ destroyOnClose
+ >
+
+
+ Change Password
+
);
}
@@ -160,28 +193,30 @@ export class UserEdit extends React.Component {
);
}
- renderPasswordOptions() {
- const { sendingPasswordEmail, passwordResetLink, resendingInvitation } = this.state;
+ renderResendInvitation() {
+ return (
+
+ Resend Invitation
+
+ );
+ }
+
+ renderSendPasswordReset() {
+ const { sendingPasswordEmail, passwordResetLink } = this.state;
return (
- {this.state.user.isInvitationPending ? (
-
- Resend Invitation
-
- ) : (
-
- Send Password Reset Email
-
- )}
+
+ Send Password Reset Email
+
{passwordResetLink && this.renderPasswordLinkAlert()}
);
@@ -204,23 +239,6 @@ export class UserEdit extends React.Component {
render() {
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 (
{user.name}
-
- {!user.isDisabled && (
-
- {this.renderApiKey()}
-
- {this.renderChangePasswordModal()}
- Change Password
- {currentUser.isAdmin && this.renderPasswordOptions()}
-
- )}
- {currentUser.isAdmin && this.renderToggleUser()}
+ {this.renderBasicInfoForm()}
+ {userPolicy.canViewApiKey(user) && this.renderApiKey()}
+ {userPolicy.canChangePassword(user) && this.renderChangePassword()}
+ {userPolicy.canResendInvitation(user) && this.renderResendInvitation()}
+ {userPolicy.canSendPasswordResetEmail(user) && this.renderSendPasswordReset()}
+ {userPolicy.canToggleUser(user) && this.renderToggleUser()}
);
}
diff --git a/client/app/services/policy/UserPolicy.js b/client/app/services/policy/UserPolicy.js
new file mode 100644
index 0000000000..9a22a70f64
--- /dev/null
+++ b/client/app/services/policy/UserPolicy.js
@@ -0,0 +1,33 @@
+import { currentUser } from '@/services/auth';
+
+/* eslint-disable class-methods-use-this */
+
+export default class UserPolicy {
+ canEditUser(user) {
+ return user && (user.id === currentUser.id || currentUser.isAdmin);
+ }
+
+ canEditBasicInfo(user) {
+ return this.canEditUser(user) && !user.isDisabled;
+ }
+
+ canViewApiKey(user) {
+ return this.canEditUser(user) && !user.isDisabled;
+ }
+
+ canChangePassword(user) {
+ return this.canEditUser(user) && !user.isDisabled;
+ }
+
+ canResendInvitation(user) {
+ return user && currentUser.isAdmin && user.isInvitationPending;
+ }
+
+ canSendPasswordResetEmail(user) {
+ return user && currentUser.isAdmin && !user.isInvitationPending;
+ }
+
+ canToggleUser(user) {
+ return user && currentUser.isAdmin;
+ }
+}
diff --git a/client/app/services/policy/index.js b/client/app/services/policy/index.js
index 9a4ebd0b1d..d5c16a2b72 100644
--- a/client/app/services/policy/index.js
+++ b/client/app/services/policy/index.js
@@ -1,7 +1,9 @@
import DefaultPolicy from './DefaultPolicy';
+import UserPolicy from './UserPolicy';
// eslint-disable-next-line import/no-mutable-exports
export let policy = new DefaultPolicy();
+export const userPolicy = new UserPolicy();
export function setPolicy(newPolicy) {
policy = newPolicy;
From ddd97587111e11cc520d7a2da941ea55e3894ae5 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 3 Feb 2019 19:36:17 -0200
Subject: [PATCH 19/35] Remove UserPolicy
---
client/app/components/users/UserEdit.jsx | 21 +++++++++------
client/app/services/policy/UserPolicy.js | 33 ------------------------
client/app/services/policy/index.js | 2 --
3 files changed, 13 insertions(+), 43 deletions(-)
delete mode 100644 client/app/services/policy/UserPolicy.js
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index d2fd4008ca..d5b13933e6 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -8,8 +8,7 @@ import Tooltip from 'antd/lib/tooltip';
import Modal from 'antd/lib/modal';
import { react2angular } from 'react2angular';
import { User } from '@/services/user';
-import { userPolicy } from '@/services/policy';
-import { clientConfig } from '@/services/auth';
+import { currentUser, clientConfig } from '@/services/auth';
import { absoluteUrl } from '@/services/utils';
import { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';
@@ -124,7 +123,7 @@ export class UserEdit extends React.Component {
return (
);
@@ -250,11 +249,17 @@ export class UserEdit extends React.Component {
{user.name}
{this.renderBasicInfoForm()}
- {userPolicy.canViewApiKey(user) && this.renderApiKey()}
- {userPolicy.canChangePassword(user) && this.renderChangePassword()}
- {userPolicy.canResendInvitation(user) && this.renderResendInvitation()}
- {userPolicy.canSendPasswordResetEmail(user) && this.renderSendPasswordReset()}
- {userPolicy.canToggleUser(user) && this.renderToggleUser()}
+ {!user.isDisabled && (
+
+ {this.renderApiKey()}
+ {this.renderChangePassword()}
+ {currentUser.isAdmin && (
+ user.isInvitationPending ?
+ this.renderResendInvitation() : this.renderSendPasswordReset()
+ )}
+
+ )}
+ {currentUser.isAdmin && this.renderToggleUser()}
);
}
diff --git a/client/app/services/policy/UserPolicy.js b/client/app/services/policy/UserPolicy.js
deleted file mode 100644
index 9a22a70f64..0000000000
--- a/client/app/services/policy/UserPolicy.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { currentUser } from '@/services/auth';
-
-/* eslint-disable class-methods-use-this */
-
-export default class UserPolicy {
- canEditUser(user) {
- return user && (user.id === currentUser.id || currentUser.isAdmin);
- }
-
- canEditBasicInfo(user) {
- return this.canEditUser(user) && !user.isDisabled;
- }
-
- canViewApiKey(user) {
- return this.canEditUser(user) && !user.isDisabled;
- }
-
- canChangePassword(user) {
- return this.canEditUser(user) && !user.isDisabled;
- }
-
- canResendInvitation(user) {
- return user && currentUser.isAdmin && user.isInvitationPending;
- }
-
- canSendPasswordResetEmail(user) {
- return user && currentUser.isAdmin && !user.isInvitationPending;
- }
-
- canToggleUser(user) {
- return user && currentUser.isAdmin;
- }
-}
diff --git a/client/app/services/policy/index.js b/client/app/services/policy/index.js
index d5c16a2b72..9a4ebd0b1d 100644
--- a/client/app/services/policy/index.js
+++ b/client/app/services/policy/index.js
@@ -1,9 +1,7 @@
import DefaultPolicy from './DefaultPolicy';
-import UserPolicy from './UserPolicy';
// eslint-disable-next-line import/no-mutable-exports
export let policy = new DefaultPolicy();
-export const userPolicy = new UserPolicy();
export function setPolicy(newPolicy) {
policy = newPolicy;
From f9984e08c10241a038de3eb2634a52594c74c6d7 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Mon, 4 Feb 2019 00:45:53 -0200
Subject: [PATCH 20/35] Create Edit Profile spec
---
.circleci/config.yml | 2 --
client/app/components/users/UserEdit.jsx | 9 +++++++--
cypress/integration/percy/page_screenshots.js | 14 ++++++++++++--
cypress/integration/user/edit_profile_spec.js | 17 +++++++++++++++++
4 files changed, 36 insertions(+), 6 deletions(-)
create mode 100644 cypress/integration/user/edit_profile_spec.js
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1242d7cf18..ac7e5bb0d1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -73,8 +73,6 @@ jobs:
command: |
npm run cypress start
docker-compose run cypress node ./cypress/cypress.js db-seed
- # Make sure the API key is the same so Percy snapshots are consistent
- docker-compose -p cypress run postgres psql -U postgres -h postgres -c "update users set api_key = 'secret' where email ='admin@redash.io';"
- run:
name: Execute Cypress tests
command: npm run cypress run-ci
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index d5b13933e6..02a488f708 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -158,7 +158,12 @@ export class UserEdit extends React.Component {
const regenerateButton = (
-
+
);
@@ -166,7 +171,7 @@ export class UserEdit extends React.Component {
-
+
);
diff --git a/cypress/integration/percy/page_screenshots.js b/cypress/integration/percy/page_screenshots.js
index 8170944007..a887b04fdc 100644
--- a/cypress/integration/percy/page_screenshots.js
+++ b/cypress/integration/percy/page_screenshots.js
@@ -6,16 +6,26 @@ const pages = [
{ name: 'Group', url: '/groups/1' },
{ name: 'Create Destination - Types', url: '/destinations/new' },
{ name: 'Organization Settings', url: '/settings/organization' },
- { name: 'User Profile', url: '/users/me' },
];
describe('Percy Page Screenshots', () => {
+ beforeEach(() => {
+ cy.login();
+ });
+
pages.forEach((page) => {
it(`takes a screenshot of ${page.name}`, () => {
- cy.login();
cy.visit(page.url);
cy.wait(1000);
cy.percySnapshot(page.name);
});
});
+
+ it('takes a screenshot of User Profile', () => {
+ cy.visit('/users/me');
+ cy.getByTestId('ApiKey').then(($apiKey) => {
+ $apiKey.val('secret');
+ });
+ cy.percySnapshot('User Profile');
+ });
});
diff --git a/cypress/integration/user/edit_profile_spec.js b/cypress/integration/user/edit_profile_spec.js
new file mode 100644
index 0000000000..1979371bba
--- /dev/null
+++ b/cypress/integration/user/edit_profile_spec.js
@@ -0,0 +1,17 @@
+describe('Edit Profile', () => {
+ beforeEach(() => {
+ cy.login();
+ cy.visit('/users/me');
+ });
+
+ it('regenerates API Key', () => {
+ cy.getByTestId('ApiKey').then(($apiKey) => {
+ const previousApiKey = $apiKey.val();
+
+ cy.getByTestId('RegenerateApiKey').click();
+ cy.get('.ant-btn-primary').contains('Regenerate').click({ force: true });
+
+ cy.getByTestId('ApiKey').should('not.eq', previousApiKey);
+ });
+ });
+});
From e4f83ab519046870c2aa79c5e8243e27e668d20d Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Mon, 4 Feb 2019 01:03:54 -0200
Subject: [PATCH 21/35] Move User profile screenshot to Edit Profile Spec
---
cypress/integration/percy/page_screenshots.js | 13 +------------
cypress/integration/user/edit_profile_spec.js | 7 +++++++
2 files changed, 8 insertions(+), 12 deletions(-)
diff --git a/cypress/integration/percy/page_screenshots.js b/cypress/integration/percy/page_screenshots.js
index a887b04fdc..15d2d54fe5 100644
--- a/cypress/integration/percy/page_screenshots.js
+++ b/cypress/integration/percy/page_screenshots.js
@@ -9,23 +9,12 @@ const pages = [
];
describe('Percy Page Screenshots', () => {
- beforeEach(() => {
- cy.login();
- });
-
pages.forEach((page) => {
it(`takes a screenshot of ${page.name}`, () => {
+ cy.login();
cy.visit(page.url);
cy.wait(1000);
cy.percySnapshot(page.name);
});
});
-
- it('takes a screenshot of User Profile', () => {
- cy.visit('/users/me');
- cy.getByTestId('ApiKey').then(($apiKey) => {
- $apiKey.val('secret');
- });
- cy.percySnapshot('User Profile');
- });
});
diff --git a/cypress/integration/user/edit_profile_spec.js b/cypress/integration/user/edit_profile_spec.js
index 1979371bba..472ebad933 100644
--- a/cypress/integration/user/edit_profile_spec.js
+++ b/cypress/integration/user/edit_profile_spec.js
@@ -14,4 +14,11 @@ describe('Edit Profile', () => {
cy.getByTestId('ApiKey').should('not.eq', previousApiKey);
});
});
+
+ it('takes a screenshot', () => {
+ cy.getByTestId('ApiKey').then(($apiKey) => {
+ $apiKey.val('secret');
+ });
+ cy.percySnapshot('User Profile');
+ });
});
From 80d5809930089ff7486a9b34a3879606cd8b8459 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Tue, 5 Feb 2019 00:03:20 -0200
Subject: [PATCH 22/35] Add tests for saving user and changing password errors
---
client/app/components/users/UserEdit.jsx | 6 ++-
cypress/integration/user/edit_profile_spec.js | 37 +++++++++++++++++++
cypress/support/commands.js | 25 +++++--------
3 files changed, 51 insertions(+), 17 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 02a488f708..165516f6ad 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -46,7 +46,7 @@ export class UserEdit extends React.Component {
if (values.password === values.password_repeat) {
this.onSaveUser(values, successCallback, errorCallback);
} else {
- errorCallback('Passwords don\'t match!');
+ errorCallback('Passwords don\'t match.');
}
}
@@ -148,7 +148,9 @@ export class UserEdit extends React.Component {
>
- Change Password
+
+ Change Password
+
);
}
diff --git a/cypress/integration/user/edit_profile_spec.js b/cypress/integration/user/edit_profile_spec.js
index 472ebad933..520505ac35 100644
--- a/cypress/integration/user/edit_profile_spec.js
+++ b/cypress/integration/user/edit_profile_spec.js
@@ -1,9 +1,30 @@
+function fillProfileDataAndSave(name, email) {
+ cy.getByTestId('Name').type(`{selectall}${name}`);
+ cy.getByTestId('Email').type(`{selectall}${email}{enter}`);
+ cy.contains('Saved.');
+}
+
+function fillChangePasswordAndSave(currentPassword, newPassword, repeatPassword) {
+ cy.getByTestId('Current Password').type(currentPassword);
+ cy.getByTestId('New Password').type(newPassword);
+ cy.getByTestId('Repeat New Password').type(`${repeatPassword}{enter}`);
+}
+
describe('Edit Profile', () => {
beforeEach(() => {
cy.login();
cy.visit('/users/me');
});
+ it('updates the user after Save', () => {
+ fillProfileDataAndSave('Jian Yang', 'jian.yang@redash.io');
+ cy.logout();
+ cy.login('jian.yang@redash.io').its('status').should('eq', 200);
+ cy.visit('/users/me');
+ cy.contains('Jian Yang');
+ fillProfileDataAndSave('Example Admin', 'admin@redash.io');
+ });
+
it('regenerates API Key', () => {
cy.getByTestId('ApiKey').then(($apiKey) => {
const previousApiKey = $apiKey.val();
@@ -21,4 +42,20 @@ describe('Edit Profile', () => {
});
cy.percySnapshot('User Profile');
});
+
+ context('changing password', () => {
+ beforeEach(() => {
+ cy.getByTestId('ChangePassword').click();
+ });
+
+ it('shows an error when current password is wrong', () => {
+ fillChangePasswordAndSave('wrongpassword', 'newpassword', 'newpassword');
+ cy.contains('Incorrect current password.');
+ });
+
+ it('shows an error when new password does not match repeat password', () => {
+ fillChangePasswordAndSave('password', 'newpassword', 'differentpassword');
+ cy.contains('Passwords don\'t match.');
+ });
+ });
});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 91edf307b6..3b5f00c8e4 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -1,19 +1,14 @@
import '@percy/cypress';
-Cypress.Commands.add('login', () => {
- const users = {
- admin: {
- email: 'admin@redash.io',
- password: 'password',
- },
- };
-
- cy.request({
- url: '/login',
- method: 'POST',
- form: true,
- body: users.admin,
- });
-});
+Cypress.Commands.add('login', (email = 'admin@redash.io', password = 'password') => cy.request({
+ url: '/login',
+ method: 'POST',
+ form: true,
+ body: {
+ email,
+ password,
+ },
+}));
+Cypress.Commands.add('logout', () => cy.request('/logout'));
Cypress.Commands.add('getByTestId', element => cy.get('[data-test="' + element + '"]'));
From 3522cdc8dc2f0e287c17645bbbd9fcf9f1c44bb4 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Tue, 5 Feb 2019 00:09:13 -0200
Subject: [PATCH 23/35] CC is back :) - Fix trailing spaces
---
cypress/integration/user/edit_profile_spec.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cypress/integration/user/edit_profile_spec.js b/cypress/integration/user/edit_profile_spec.js
index 520505ac35..3c012b4f89 100644
--- a/cypress/integration/user/edit_profile_spec.js
+++ b/cypress/integration/user/edit_profile_spec.js
@@ -31,7 +31,7 @@ describe('Edit Profile', () => {
cy.getByTestId('RegenerateApiKey').click();
cy.get('.ant-btn-primary').contains('Regenerate').click({ force: true });
-
+
cy.getByTestId('ApiKey').should('not.eq', previousApiKey);
});
});
From 74a77c51968cac51e90e7893e7fbdf479e111a4c Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Tue, 5 Feb 2019 19:31:18 -0200
Subject: [PATCH 24/35] Add test for succesful password update
---
cypress/integration/user/edit_profile_spec.js | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/cypress/integration/user/edit_profile_spec.js b/cypress/integration/user/edit_profile_spec.js
index 3c012b4f89..c7f43be70a 100644
--- a/cypress/integration/user/edit_profile_spec.js
+++ b/cypress/integration/user/edit_profile_spec.js
@@ -48,6 +48,17 @@ describe('Edit Profile', () => {
cy.getByTestId('ChangePassword').click();
});
+ it('updates user password when password is correct', () => {
+ fillChangePasswordAndSave('password', 'newpassword', 'newpassword');
+ cy.contains('Saved.');
+ cy.logout();
+ cy.login(undefined, 'newpassword').its('status').should('eq', 200);
+ cy.visit('/users/me');
+ cy.getByTestId('ChangePassword').click();
+ fillChangePasswordAndSave('newpassword', 'password', 'password');
+ cy.contains('Saved.');
+ });
+
it('shows an error when current password is wrong', () => {
fillChangePasswordAndSave('wrongpassword', 'newpassword', 'newpassword');
cy.contains('Incorrect current password.');
From a89ae71cfd494d067e804873bcbfa9b31b93334b Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Thu, 7 Feb 2019 22:53:02 -0200
Subject: [PATCH 25/35] A few improvements from code review
---
.../components/dynamic-form/DynamicForm.jsx | 11 ++--
client/app/components/users/UserEdit.jsx | 60 ++++++++++---------
2 files changed, 36 insertions(+), 35 deletions(-)
diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index 6c075894b5..977e773a9c 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -7,20 +7,19 @@ import Checkbox from 'antd/lib/checkbox';
import Button from 'antd/lib/button';
import Upload from 'antd/lib/upload';
import Icon from 'antd/lib/icon';
+import { includes } from 'lodash';
import { react2angular } from 'react2angular';
import { toastr } from '@/services/ng';
import { Field, Action, AntdForm } from '../proptypes';
import helper from './dynamicFormHelper';
-const fieldRules = ({ title, name, type, required, minLength }) => {
- const fieldLabel = title || helper.toHuman(name);
-
+const fieldRules = ({ type, required, minLength }) => {
const requiredRule = required;
- const minLengthRule = minLength && ['text', 'email', 'password'].includes(type);
+ const minLengthRule = minLength && includes(['text', 'email', 'password'], type);
return [
- requiredRule && { required, message: `${fieldLabel} is required.` },
- minLengthRule && { min: minLength, message: `${fieldLabel} is too short.` },
+ requiredRule && { required, message: 'This field is required.' },
+ minLengthRule && { min: minLength, message: 'This field is too short.' },
].filter(rule => rule);
};
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 165516f6ad..7cb5cf55f3 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -22,36 +22,15 @@ export class UserEdit extends React.Component {
super(props);
this.state = {
user: this.props.user,
- changingPassword: false,
+ passwordModalIsOpen: false,
sendingPasswordEmail: false,
resendingInvitation: false,
+ togglingUser: false,
};
}
- onSaveUser = (values, successCallback, errorCallback) => {
- const data = {
- id: this.props.user.id,
- ...values,
- };
-
- User.save(data, (user) => {
- successCallback('Saved.');
- this.setState({ user: User.convertUserInfo(user) });
- }, (error) => {
- errorCallback(error.data.message || 'Failed saving.');
- });
- };
-
- onUpdatePassword = (values, successCallback, errorCallback) => {
- if (values.password === values.password_repeat) {
- this.onSaveUser(values, successCallback, errorCallback);
- } else {
- errorCallback('Passwords don\'t match.');
- }
- }
-
onClickChangePassword = () => {
- this.setState({ changingPassword: true });
+ this.setState({ passwordModalIsOpen: true });
};
onClickSendPasswordReset = () => {
@@ -75,7 +54,8 @@ export class UserEdit extends React.Component {
onClickRegenerateApiKey = () => {
const doRegenerate = () => {
User.regenerateApiKey(this.state.user).then((apiKey) => {
- this.setState(prevState => ({ user: { ...prevState.user, apiKey } }));
+ const { user } = this.state;
+ this.setState({ user: { ...user, apiKey } });
});
};
@@ -101,6 +81,28 @@ export class UserEdit extends React.Component {
});
};
+ saveUser = (values, successCallback, errorCallback) => {
+ const data = {
+ id: this.props.user.id,
+ ...values,
+ };
+
+ User.save(data, (user) => {
+ successCallback('Saved.');
+ this.setState({ user: User.convertUserInfo(user) });
+ }, (error = {}) => {
+ errorCallback(error.data && error.data.message || 'Failed saving.');
+ });
+ };
+
+ updatePassword = (values, successCallback, errorCallback) => {
+ if (values.password === values.password_repeat) {
+ this.saveUser(values, successCallback, errorCallback);
+ } else {
+ errorCallback('Passwords don\'t match.');
+ }
+ }
+
renderBasicInfoForm() {
const { user } = this.state;
const formFields = [
@@ -124,7 +126,7 @@ export class UserEdit extends React.Component {
);
}
@@ -140,13 +142,13 @@ export class UserEdit extends React.Component {
{ this.setState({ changingPassword: false }); }}
+ onCancel={() => { this.setState({ passwordModalIsOpen: false }); }}
footer={null}
destroyOnClose
>
-
+
Change Password
From 99f317c2bad04c8a37a6994107593253ae0b63be Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Thu, 7 Feb 2019 23:00:23 -0200
Subject: [PATCH 26/35] Remove Toggle User button when seeing your own profile
---
client/app/components/users/UserEdit.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 7cb5cf55f3..1fae86404a 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -268,7 +268,7 @@ export class UserEdit extends React.Component {
)}
)}
- {currentUser.isAdmin && this.renderToggleUser()}
+ {currentUser.isAdmin && user.id !== currentUser.id && this.renderToggleUser()}
);
}
From e2f53034ddcc977459b360201c7ab6321db7ba22 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 9 Feb 2019 18:26:23 -0200
Subject: [PATCH 27/35] Create InputWithCopy
---
client/app/components/InputWithCopy.jsx | 57 +++++++++++++++++++++++++
1 file changed, 57 insertions(+)
create mode 100644 client/app/components/InputWithCopy.jsx
diff --git a/client/app/components/InputWithCopy.jsx b/client/app/components/InputWithCopy.jsx
new file mode 100644
index 0000000000..c2e4571aaa
--- /dev/null
+++ b/client/app/components/InputWithCopy.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import Input from 'antd/lib/input';
+import Icon from 'antd/lib/icon';
+import Tooltip from 'antd/lib/tooltip';
+
+export default class InputWithCopy extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { copied: null };
+ this.ref = React.createRef();
+ this.copyFeatureSupported = document.queryCommandSupported('copy');
+ this.resetCopyState = null;
+ }
+
+ componentWillUnmount() {
+ if (this.resetCopyState) {
+ clearTimeout(this.resetCopyState);
+ }
+ }
+
+ copy = () => {
+ // select text
+ this.ref.current.select();
+
+ // copy
+ try {
+ const success = document.execCommand('copy');
+ if (!success) {
+ throw new Error();
+ }
+ this.setState({ copied: 'Copied!' });
+ } catch (err) {
+ this.setState({
+ copied: 'Copy failed',
+ });
+ }
+
+ // reset tooltip
+ this.resetCopyState = setTimeout(() => this.setState({ copied: null }), 2000);
+ };
+
+ render() {
+ const copyButton = (
+
+
+
+ );
+
+ return (
+
+ );
+ }
+}
From e2ed0a43e4ed848e453a993777876544da78a3d0 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 9 Feb 2019 18:33:28 -0200
Subject: [PATCH 28/35] Fix possible errors when network is off and improve
Email not sent alert
---
client/app/components/users/UserEdit.jsx | 34 +++++++++++++++---------
client/app/services/user.js | 9 ++++++-
2 files changed, 29 insertions(+), 14 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index 1fae86404a..cec80ecc7a 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -8,10 +8,11 @@ import Tooltip from 'antd/lib/tooltip';
import Modal from 'antd/lib/modal';
import { react2angular } from 'react2angular';
import { User } from '@/services/user';
-import { currentUser, clientConfig } from '@/services/auth';
+import { currentUser } from '@/services/auth';
import { absoluteUrl } from '@/services/utils';
import { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';
+import InputWithCopy from '../InputWithCopy';
export class UserEdit extends React.Component {
static propTypes = {
@@ -54,8 +55,10 @@ export class UserEdit extends React.Component {
onClickRegenerateApiKey = () => {
const doRegenerate = () => {
User.regenerateApiKey(this.state.user).then((apiKey) => {
- const { user } = this.state;
- this.setState({ user: { ...user, apiKey } });
+ if (apiKey) {
+ const { user } = this.state;
+ this.setState({ user: { ...user, apiKey } });
+ }
});
};
@@ -74,8 +77,10 @@ export class UserEdit extends React.Component {
const toggleUser = user.isDisabled ? User.enableUser : User.disableUser;
this.setState({ togglingUser: true });
- toggleUser(user).then(({ data }) => {
- this.setState({ user: User.convertUserInfo(data) });
+ toggleUser(user).then((data) => {
+ if (data) {
+ this.setState({ user: User.convertUserInfo(data.data) });
+ }
}).finally(() => {
this.setState({ togglingUser: false });
});
@@ -186,14 +191,17 @@ export class UserEdit extends React.Component {
return (
- The mail server is not configured, please send the following link
- to {user.name} to reset their password:
-
-
- ) : 'The user should receive a link to reset their password by email soon.'}
- type="success"
+ message="Email not sent!"
+ description={(
+
+
+ The mail server is not configured, please send the following link
+ to {user.name} to reset their password:
+
+
+
+ )}
+ type="warning"
className="m-t-20"
afterClose={() => { this.setState({ passwordResetLink: null }); }}
closable
diff --git a/client/app/services/user.js b/client/app/services/user.js
index e6d44e6b26..76f206e4fb 100644
--- a/client/app/services/user.js
+++ b/client/app/services/user.js
@@ -1,5 +1,6 @@
import { isString } from 'lodash';
import { $http, $sanitize, toastr } from '@/services/ng';
+import { clientConfig } from '@/services/auth';
export let User = null; // eslint-disable-line import/no-mutable-exports
@@ -97,7 +98,13 @@ function regenerateApiKey(user) {
function sendPasswordReset(user) {
return $http
.post(`api/users/${user.id}/reset_password`)
- .then(({ data }) => data.reset_link)
+ .then(({ data }) => {
+ if (clientConfig.mailSettingsMissing) {
+ toastr.warning('The mail server is not configured.');
+ return data.reset_link;
+ }
+ toastr.success('Password reset email sent.');
+ })
.catch((response) => {
const message =
response.message
From 071c968596c148b0dabdf37c716af4c70e854fc7 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sat, 9 Feb 2019 19:18:37 -0200
Subject: [PATCH 29/35] Add default response object for $http possible errors
---
client/app/services/user.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/client/app/services/user.js b/client/app/services/user.js
index 76f206e4fb..bc04fd7838 100644
--- a/client/app/services/user.js
+++ b/client/app/services/user.js
@@ -38,7 +38,7 @@ function disableUser(user) {
user.profile_image_url = data.data.profile_image_url;
return data;
})
- .catch((response) => {
+ .catch((response = {}) => {
const message =
response.data && response.data.message
? response.data.message
@@ -56,7 +56,7 @@ function deleteUser(user) {
toastr.warning(`User ${userName} has been deleted.`, { allowHtml: true });
return data;
})
- .catch((response) => {
+ .catch((response = {}) => {
const message =
response.data && response.data.message
? response.data.message
@@ -85,7 +85,7 @@ function regenerateApiKey(user) {
toastr.success('The API Key has been updated.');
return data.api_key;
})
- .catch((response) => {
+ .catch((response = {}) => {
const message =
response.data && response.data.message
? response.data.message
@@ -105,7 +105,7 @@ function sendPasswordReset(user) {
}
toastr.success('Password reset email sent.');
})
- .catch((response) => {
+ .catch((response = {}) => {
const message =
response.message
? response.message
@@ -121,7 +121,7 @@ function resendInvitation(user) {
.then(() => {
toastr.success('Invitation sent.');
})
- .catch((response) => {
+ .catch((response = {}) => {
const message =
response.message
? response.message
From f046914d8da0af885119a102498193fbfc468c60 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 10 Feb 2019 13:09:34 -0200
Subject: [PATCH 30/35] Changes in UserEdit - removed onClick from methods name
- regenerate API Key now uses InputWithCopy - Password title added
---
client/app/components/users/UserEdit.jsx | 56 ++++++++++++------------
1 file changed, 27 insertions(+), 29 deletions(-)
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index cec80ecc7a..c969b722c6 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -2,9 +2,6 @@ import React, { Fragment } from 'react';
import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button';
import Form from 'antd/lib/form';
-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';
@@ -24,17 +21,18 @@ export class UserEdit extends React.Component {
this.state = {
user: this.props.user,
passwordModalIsOpen: false,
+ regeneratingApiKey: false,
sendingPasswordEmail: false,
resendingInvitation: false,
togglingUser: false,
};
}
- onClickChangePassword = () => {
+ changePassword = () => {
this.setState({ passwordModalIsOpen: true });
};
- onClickSendPasswordReset = () => {
+ sendPasswordReset = () => {
this.setState({ sendingPasswordEmail: true });
User.sendPasswordReset(this.state.user).then((passwordResetLink) => {
@@ -44,7 +42,7 @@ export class UserEdit extends React.Component {
});
};
- onClickResendInvitation = () => {
+ resendInvitation = () => {
this.setState({ resendingInvitation: true });
User.resendInvitation(this.state.user).finally(() => {
@@ -52,13 +50,16 @@ export class UserEdit extends React.Component {
});
};
- onClickRegenerateApiKey = () => {
+ regenerateApiKey = () => {
const doRegenerate = () => {
+ this.setState({ regeneratingApiKey: true });
User.regenerateApiKey(this.state.user).then((apiKey) => {
if (apiKey) {
const { user } = this.state;
this.setState({ user: { ...user, apiKey } });
}
+ }).finally(() => {
+ this.setState({ regeneratingApiKey: false });
});
};
@@ -72,7 +73,7 @@ export class UserEdit extends React.Component {
});
};
- onClickToggleUser = () => {
+ toggleUser = () => {
const { user } = this.state;
const toggleUser = user.isDisabled ? User.enableUser : User.disableUser;
@@ -145,7 +146,6 @@ export class UserEdit extends React.Component {
return (
-
-
+
Change Password
@@ -163,24 +163,20 @@ export class UserEdit extends React.Component {
}
renderApiKey() {
- const { user } = this.state;
-
- const regenerateButton = (
-
-
-
- );
+ const { user, regeneratingApiKey } = this.state;
return (
-
+
+
+ Regenerate
+
);
@@ -213,7 +209,7 @@ export class UserEdit extends React.Component {
return (
Resend Invitation
@@ -228,7 +224,7 @@ export class UserEdit extends React.Component {
Send Password Reset Email
@@ -238,15 +234,15 @@ export class UserEdit extends React.Component {
);
}
- renderToggleUser() {
+ rendertoggleUser() {
const { user, togglingUser } = this.state;
return user.isDisabled ? (
-
+
Enable User
) : (
-
+
Disable User
);
@@ -269,6 +265,7 @@ export class UserEdit extends React.Component {
{!user.isDisabled && (
{this.renderApiKey()}
+ Password
{this.renderChangePassword()}
{currentUser.isAdmin && (
user.isInvitationPending ?
@@ -276,7 +273,8 @@ export class UserEdit extends React.Component {
)}
)}
- {currentUser.isAdmin && user.id !== currentUser.id && this.renderToggleUser()}
+
+ {currentUser.isAdmin && user.id !== currentUser.id && this.rendertoggleUser()}
);
}
From 02899239b412fde93c742524e7f621d716e058e5 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Sun, 10 Feb 2019 17:30:23 -0200
Subject: [PATCH 31/35] Update UserEdit render behavior and styling - Password
title changed to h5 - change rendering rules for actions - Password modal is
now closed when password is changed - change DynamicForm readOnly to the
fields and add hideSubmitButton
---
.../components/dynamic-form/DynamicForm.jsx | 12 +++---
client/app/components/proptypes.js | 1 +
client/app/components/users/UserEdit.jsx | 37 +++++++++++--------
3 files changed, 28 insertions(+), 22 deletions(-)
diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx
index 977e773a9c..1d030558da 100644
--- a/client/app/components/dynamic-form/DynamicForm.jsx
+++ b/client/app/components/dynamic-form/DynamicForm.jsx
@@ -28,7 +28,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
fields: PropTypes.arrayOf(Field),
actions: PropTypes.arrayOf(Action),
feedbackIcons: PropTypes.bool,
- readOnly: PropTypes.bool,
+ hideSubmitButton: PropTypes.bool,
saveText: PropTypes.string,
onSubmit: PropTypes.func,
form: AntdForm.isRequired,
@@ -38,7 +38,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
fields: [],
actions: [],
feedbackIcons: false,
- readOnly: false,
+ hideSubmitButton: false,
saveText: 'Save',
onSubmit: () => {},
};
@@ -155,9 +155,9 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
return this.props.fields.map((field) => {
const [firstField] = this.props.fields;
const FormItem = Form.Item;
- const { name, title, type } = field;
+ const { name, title, type, readOnly } = field;
const fieldLabel = title || helper.toHuman(name);
- const { feedbackIcons, readOnly } = this.props;
+ const { feedbackIcons } = this.props;
const formItemProps = {
key: name,
@@ -207,8 +207,8 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
disabled: this.state.isSubmitting,
loading: this.state.isSubmitting,
};
- const { readOnly, saveText } = this.props;
- const saveButton = !readOnly;
+ const { hideSubmitButton, saveText } = this.props;
+ const saveButton = !hideSubmitButton;
return (
+
-
- Regenerate
-
+
+ Regenerate
+
);
}
@@ -265,9 +269,10 @@ export class UserEdit extends React.Component {
{!user.isDisabled && (
{this.renderApiKey()}
- Password
- {this.renderChangePassword()}
- {currentUser.isAdmin && (
+
+ Password
+ {user.id === currentUser.id && this.renderChangePassword()}
+ {(currentUser.isAdmin && user.id !== currentUser.id) && (
user.isInvitationPending ?
this.renderResendInvitation() : this.renderSendPasswordReset()
)}
From 2e8756fedeaed03e6f94959dfec797d0d11eb52d Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Wed, 13 Feb 2019 15:22:50 -0200
Subject: [PATCH 32/35] Create ChangePasswordDialog and update UserEdit
---
.../components/users/ChangePasswordDialog.jsx | 146 ++++++++++++++++++
client/app/components/users/UserEdit.jsx | 48 +-----
cypress/integration/user/edit_profile_spec.js | 6 +-
3 files changed, 156 insertions(+), 44 deletions(-)
create mode 100644 client/app/components/users/ChangePasswordDialog.jsx
diff --git a/client/app/components/users/ChangePasswordDialog.jsx b/client/app/components/users/ChangePasswordDialog.jsx
new file mode 100644
index 0000000000..4b0193770e
--- /dev/null
+++ b/client/app/components/users/ChangePasswordDialog.jsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import Form from 'antd/lib/form';
+import Modal from 'antd/lib/modal';
+import Input from 'antd/lib/input';
+import { isFunction } from 'lodash';
+import { User } from '@/services/user';
+import { toastr } from '@/services/ng';
+import { UserProfile } from '../proptypes';
+import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
+
+class ChangePasswordDialog extends React.Component {
+ static propTypes = {
+ user: UserProfile.isRequired,
+ dialog: DialogPropType.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ currentPassword: { value: '', error: null, touched: false },
+ newPassword: { value: '', error: null, touched: false },
+ repeatPassword: { value: '', error: null, touched: false },
+ updatingPassword: false,
+ };
+ }
+
+ fieldError = (name, value) => {
+ if (value.length === 0) return 'This field is required.';
+ if (name !== 'currentPassword' && value.length < 6) return 'This field is too short.';
+ if (name === 'repeatPassword' && value !== this.state.newPassword.value) return 'Password don\'t match';
+ return null;
+ };
+
+ validateFields = (callback) => {
+ const { currentPassword, newPassword, repeatPassword } = this.state;
+
+ const errors = {
+ currentPassword: this.fieldError('currentPassword', currentPassword.value),
+ newPassword: this.fieldError('newPassword', newPassword.value),
+ repeatPassword: this.fieldError('repeatPassword', repeatPassword.value),
+ };
+
+ this.setState({
+ currentPassword: { ...currentPassword, error: errors.currentPassword },
+ newPassword: { ...newPassword, error: errors.newPassword },
+ repeatPassword: { ...repeatPassword, error: errors.repeatPassword },
+ });
+
+ if (isFunction(callback)) {
+ if (errors.currentPassword || errors.newPassword || errors.repeatPassword) {
+ callback(errors);
+ } else callback(null);
+ }
+ }
+
+ updatePassword = () => {
+ const { currentPassword, newPassword, updatingPassword } = this.state;
+
+ if (!updatingPassword) {
+ this.validateFields((err) => {
+ if (!err) {
+ const userData = {
+ id: this.props.user.id,
+ old_password: currentPassword.value,
+ password: newPassword.value,
+ };
+
+ this.setState({ updatingPassword: true });
+
+ User.save(userData, () => {
+ toastr.success('Saved.');
+ this.props.dialog.close({ success: true });
+ }, (error = {}) => {
+ toastr.error(error.data && error.data.message || 'Failed saving.');
+ this.setState({ updatingPassword: false });
+ });
+ } else {
+ this.setState(prevState => ({
+ currentPassword: { ...prevState.currentPassword, touched: true },
+ newPassword: { ...prevState.newPassword, touched: true },
+ repeatPassword: { ...prevState.repeatPassword, touched: true },
+ }));
+ }
+ });
+ }
+ }
+
+ handleChange = (e) => {
+ const { name, value } = e.target;
+ const { error } = this.state[name];
+
+ this.setState({ [name]: { value, error, touched: true } }, () => {
+ this.validateFields();
+ });
+ }
+
+ render() {
+ const { dialog } = this.props;
+ const { currentPassword, newPassword, repeatPassword, updatingPassword } = this.state;
+
+ const formItemProps = { className: 'm-b-10', required: true };
+
+ const inputProps = {
+ onChange: this.handleChange,
+ onPressEnter: this.updatePassword,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default wrapDialog(ChangePasswordDialog);
diff --git a/client/app/components/users/UserEdit.jsx b/client/app/components/users/UserEdit.jsx
index e86b6b8c0b..e4f06cbc7f 100644
--- a/client/app/components/users/UserEdit.jsx
+++ b/client/app/components/users/UserEdit.jsx
@@ -9,6 +9,7 @@ import { currentUser } from '@/services/auth';
import { absoluteUrl } from '@/services/utils';
import { UserProfile } from '../proptypes';
import { DynamicForm } from '../dynamic-form/DynamicForm';
+import ChangePasswordDialog from './ChangePasswordDialog';
import InputWithCopy from '../InputWithCopy';
export class UserEdit extends React.Component {
@@ -20,7 +21,6 @@ export class UserEdit extends React.Component {
super(props);
this.state = {
user: this.props.user,
- passwordModalIsOpen: false,
regeneratingApiKey: false,
sendingPasswordEmail: false,
resendingInvitation: false,
@@ -29,7 +29,7 @@ export class UserEdit extends React.Component {
}
changePassword = () => {
- this.setState({ passwordModalIsOpen: true });
+ ChangePasswordDialog.showModal({ user: this.props.user });
};
sendPasswordReset = () => {
@@ -101,19 +101,6 @@ export class UserEdit extends React.Component {
});
};
- updatePassword = (values, successCallback, errorCallback) => {
- const updatePasswordSuccess = (message) => {
- this.setState({ passwordModalIsOpen: false });
- successCallback(message);
- };
-
- if (values.password === values.password_repeat) {
- this.saveUser(values, updatePasswordSuccess, errorCallback);
- } else {
- errorCallback('Passwords don\'t match.');
- }
- }
-
renderBasicInfoForm() {
const { user } = this.state;
const formFields = [
@@ -140,31 +127,6 @@ export class UserEdit extends React.Component {
);
}
- renderChangePassword() {
- const fields = [
- { name: 'old_password', title: 'Current Password' },
- { name: 'password', title: 'New Password', minLength: 6 },
- { name: 'password_repeat', title: 'Repeat New Password' },
- ].map(field => ({ ...field, type: 'password', required: true }));
-
- return (
-
- { this.setState({ passwordModalIsOpen: false }); }}
- footer={null}
- destroyOnClose
- >
-
-
-
- Change Password
-
-
- );
- }
-
renderApiKey() {
const { user, regeneratingApiKey } = this.state;
@@ -271,7 +233,11 @@ export class UserEdit extends React.Component {
{this.renderApiKey()}
Password
- {user.id === currentUser.id && this.renderChangePassword()}
+ {user.id === currentUser.id && (
+
+ Change Password
+
+ )}
{(currentUser.isAdmin && user.id !== currentUser.id) && (
user.isInvitationPending ?
this.renderResendInvitation() : this.renderSendPasswordReset()
diff --git a/cypress/integration/user/edit_profile_spec.js b/cypress/integration/user/edit_profile_spec.js
index c7f43be70a..6c6753837b 100644
--- a/cypress/integration/user/edit_profile_spec.js
+++ b/cypress/integration/user/edit_profile_spec.js
@@ -5,9 +5,9 @@ function fillProfileDataAndSave(name, email) {
}
function fillChangePasswordAndSave(currentPassword, newPassword, repeatPassword) {
- cy.getByTestId('Current Password').type(currentPassword);
- cy.getByTestId('New Password').type(newPassword);
- cy.getByTestId('Repeat New Password').type(`${repeatPassword}{enter}`);
+ cy.getByTestId('CurrentPassword').type(currentPassword);
+ cy.getByTestId('NewPassword').type(newPassword);
+ cy.getByTestId('RepeatPassword').type(`${repeatPassword}{enter}`);
}
describe('Edit Profile', () => {
From f6dfdabe78c1f37c4ebc7531f3144dafc7d793d7 Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Wed, 13 Feb 2019 15:23:07 -0200
Subject: [PATCH 33/35] Fix possible console error
---
client/app/pages/users/show.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/app/pages/users/show.html b/client/app/pages/users/show.html
index 5c5d2b9ad2..7c28a45149 100644
--- a/client/app/pages/users/show.html
+++ b/client/app/pages/users/show.html
@@ -3,7 +3,7 @@
From d5a3c8f4c0834fb348be0aa60669f7d4a37d5c0b Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Wed, 13 Feb 2019 15:44:42 -0200
Subject: [PATCH 34/35] Remove password match assertion from spec
---
cypress/integration/user/edit_profile_spec.js | 5 -----
1 file changed, 5 deletions(-)
diff --git a/cypress/integration/user/edit_profile_spec.js b/cypress/integration/user/edit_profile_spec.js
index 6c6753837b..99d3d58ee3 100644
--- a/cypress/integration/user/edit_profile_spec.js
+++ b/cypress/integration/user/edit_profile_spec.js
@@ -63,10 +63,5 @@ describe('Edit Profile', () => {
fillChangePasswordAndSave('wrongpassword', 'newpassword', 'newpassword');
cy.contains('Incorrect current password.');
});
-
- it('shows an error when new password does not match repeat password', () => {
- fillChangePasswordAndSave('password', 'newpassword', 'differentpassword');
- cy.contains('Passwords don\'t match.');
- });
});
});
From 230f7e38fd97d07a06bdfcd44451560e52f06aaa Mon Sep 17 00:00:00 2001
From: Gabriel Dutra
Date: Thu, 14 Feb 2019 13:34:04 -0200
Subject: [PATCH 35/35] Fix typo
---
client/app/components/users/ChangePasswordDialog.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/app/components/users/ChangePasswordDialog.jsx b/client/app/components/users/ChangePasswordDialog.jsx
index 4b0193770e..eb0b652462 100644
--- a/client/app/components/users/ChangePasswordDialog.jsx
+++ b/client/app/components/users/ChangePasswordDialog.jsx
@@ -27,7 +27,7 @@ class ChangePasswordDialog extends React.Component {
fieldError = (name, value) => {
if (value.length === 0) return 'This field is required.';
if (name !== 'currentPassword' && value.length < 6) return 'This field is too short.';
- if (name === 'repeatPassword' && value !== this.state.newPassword.value) return 'Password don\'t match';
+ if (name === 'repeatPassword' && value !== this.state.newPassword.value) return 'Passwords don\'t match';
return null;
};