Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Migrate User Pages to React #3506

Merged
merged 20 commits into from
Mar 4, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions client/app/components/EmailSettingsWarning.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { currentUser, clientConfig } from '@/services/auth';

export function EmailSettingsWarning({ featureName }) {
return (clientConfig.mailSettingsMissing && currentUser.isAdmin) ? (
<p className="alert alert-danger">
{`It looks like your mail server isn't configured. Make sure to configure it for the ${featureName} to work.`}
</p>
) : null;
}

EmailSettingsWarning.propTypes = {
featureName: PropTypes.string.isRequired,
};

export default function init(ngModule) {
ngModule.component('emailSettingsWarning', react2angular(EmailSettingsWarning));
}

init.init = true;
3 changes: 3 additions & 0 deletions client/app/components/dynamic-form/DynamicForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import helper from './dynamicFormHelper';
const fieldRules = ({ type, required, minLength }) => {
const requiredRule = required;
const minLengthRule = minLength && includes(['text', 'email', 'password'], type);
const emailTypeRule = type === 'email';

return [
requiredRule && { required, message: 'This field is required.' },
minLengthRule && { min: minLength, message: 'This field is too short.' },
emailTypeRule && { type: 'email', message: 'This field must be a valid email.' },
].filter(rule => rule);
};

Expand Down Expand Up @@ -167,6 +169,7 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone
};

const fieldProps = {
...field.props,
autoFocus: (firstField === field),
className: 'w-100',
name,
Expand Down
16 changes: 0 additions & 16 deletions client/app/components/email-settings-warning/index.js

This file was deleted.

21 changes: 0 additions & 21 deletions client/app/components/error-messages.js

This file was deleted.

1 change: 1 addition & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const Field = PropTypes.shape({
readOnly: PropTypes.bool,
minLength: PropTypes.number,
placeholder: PropTypes.string,
props: PropTypes.object, // eslint-disable-line react/forbid-prop-types
});

export const Action = PropTypes.shape({
Expand Down
60 changes: 60 additions & 0 deletions client/app/components/users/CreateUserDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'antd/lib/modal';
import Alert from 'antd/lib/alert';
import { DynamicForm } from '@/components/dynamic-form/DynamicForm';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import recordEvent from '@/services/recordEvent';

class CreateUserDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
onCreate: PropTypes.func.isRequired,
};

constructor(props) {
super(props);
this.state = { savingUser: false, errorMessage: null };
this.form = React.createRef();
}

componentDidMount() {
recordEvent('view', 'page', 'users/new');
Copy link
Member Author

Choose a reason for hiding this comment

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

I was not sure if I should keep this, but I did in case anyone uses it to track the event.

Lmk in case I should remove it now that this is a Dialog and not a page.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's okay to keep it

}

createUser = () => {
this.form.current.validateFieldsAndScroll((err, values) => {
if (!err) {
this.setState({ savingUser: true });
this.props.onCreate(values).then(() => {
this.props.dialog.close();
}).catch((errorMessage) => {
this.setState({ savingUser: false, errorMessage });
});
}
});
};

render() {
const { savingUser, errorMessage } = this.state;
const formFields = [
{ name: 'name', title: 'Name', type: 'text' },
{ name: 'email', title: 'Email', type: 'email' },
].map(field => ({ required: true, props: { onPressEnter: this.createUser }, ...field }));

return (
<Modal
{...this.props.dialog.props}
title="Create a New User"
okText="Create"
okButtonProps={{ loading: savingUser }}
onOk={() => this.createUser()}
>
<DynamicForm fields={formFields} ref={this.form} hideSubmitButton />
{errorMessage && <Alert message={errorMessage} type="error" showIcon />}
</Modal>
);
}
}

export default wrapDialog(CreateUserDialog);
37 changes: 17 additions & 20 deletions client/app/components/users/UserEdit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Alert from 'antd/lib/alert';
import Button from 'antd/lib/button';
import Form from 'antd/lib/form';
import Modal from 'antd/lib/modal';
import { react2angular } from 'react2angular';
import { User } from '@/services/user';
import { currentUser } from '@/services/auth';
import { absoluteUrl } from '@/services/utils';
Expand All @@ -12,7 +11,7 @@ import { DynamicForm } from '../dynamic-form/DynamicForm';
import ChangePasswordDialog from './ChangePasswordDialog';
import InputWithCopy from '../InputWithCopy';

export class UserEdit extends React.Component {
export default class UserEdit extends React.Component {
static propTypes = {
user: UserProfile.isRequired,
};
Expand All @@ -35,8 +34,8 @@ export class UserEdit extends React.Component {
sendPasswordReset = () => {
this.setState({ sendingPasswordEmail: true });

User.sendPasswordReset(this.state.user).then((passwordResetLink) => {
this.setState({ passwordResetLink });
User.sendPasswordReset(this.state.user).then((passwordLink) => {
this.setState({ passwordLink });
}).finally(() => {
this.setState({ sendingPasswordEmail: false });
});
Expand All @@ -45,7 +44,9 @@ export class UserEdit extends React.Component {
resendInvitation = () => {
this.setState({ resendingInvitation: true });

User.resendInvitation(this.state.user).finally(() => {
User.resendInvitation(this.state.user).then((passwordLink) => {
this.setState({ passwordLink });
}).finally(() => {
this.setState({ resendingInvitation: false });
});
};
Expand Down Expand Up @@ -149,7 +150,7 @@ export class UserEdit extends React.Component {
}

renderPasswordLinkAlert() {
const { user, passwordResetLink } = this.state;
const { user, passwordLink } = this.state;

return (
<Alert
Expand All @@ -158,14 +159,14 @@ export class UserEdit extends React.Component {
<Fragment>
<p>
The mail server is not configured, please send the following link
to <b>{user.name}</b> to reset their password:
to <b>{user.name}</b>:
</p>
<InputWithCopy value={absoluteUrl(passwordResetLink)} readOnly />
<InputWithCopy value={absoluteUrl(passwordLink)} readOnly />
</Fragment>
)}
type="warning"
className="m-t-20"
afterClose={() => { this.setState({ passwordResetLink: null }); }}
afterClose={() => { this.setState({ passwordLink: null }); }}
closable
/>
);
Expand All @@ -184,7 +185,7 @@ export class UserEdit extends React.Component {
}

renderSendPasswordReset() {
const { sendingPasswordEmail, passwordResetLink } = this.state;
const { sendingPasswordEmail } = this.state;

return (
<Fragment>
Expand All @@ -195,7 +196,6 @@ export class UserEdit extends React.Component {
>
Send Password Reset Email
</Button>
{passwordResetLink && this.renderPasswordLinkAlert()}
</Fragment>
);
}
Expand All @@ -215,7 +215,7 @@ export class UserEdit extends React.Component {
}

render() {
const { user } = this.state;
const { user, passwordLink } = this.state;

return (
<div className="col-md-4 col-md-offset-4">
Expand All @@ -239,8 +239,11 @@ export class UserEdit extends React.Component {
</Button>
)}
{(currentUser.isAdmin && user.id !== currentUser.id) && (
user.isInvitationPending ?
this.renderResendInvitation() : this.renderSendPasswordReset()
<Fragment>
{user.isInvitationPending ?
this.renderResendInvitation() : this.renderSendPasswordReset()}
{passwordLink && this.renderPasswordLinkAlert()}
</Fragment>
)}
</Fragment>
)}
Expand All @@ -250,9 +253,3 @@ export class UserEdit extends React.Component {
);
}
}

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

init.init = true;
9 changes: 2 additions & 7 deletions client/app/components/users/UserShow.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import { react2angular } from 'react2angular';
import { UserProfile } from '../proptypes';

export const UserShow = ({ user: { name, email, profileImageUrl } }) => (
const UserShow = ({ user: { name, email, profileImageUrl } }) => (
<div className="col-md-4 col-md-offset-4 profile__container">
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
<img
alt="profile"
Expand All @@ -28,8 +27,4 @@ UserShow.propTypes = {
user: UserProfile.isRequired,
};

export default function init(ngModule) {
ngModule.component('userShow', react2angular(UserShow));
}

init.init = true;
export default UserShow;
2 changes: 1 addition & 1 deletion client/app/components/users/UserShow.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { UserShow } from './UserShow';
import UserShow from './UserShow';

test('renders correctly', () => {
const user = {
Expand Down
2 changes: 1 addition & 1 deletion client/app/pages/alert/alert.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="container">
<page-header title="$ctrl.alert.name || $ctrl.getDefaultName() || 'New Alert'"></page-header>

<email-settings-warning function="'alert emails'"></email-settings-warning>
<email-settings-warning feature-name="'alert emails'"></email-settings-warning>

<div class="container">
<div class="row bg-white tiled p-10">
Expand Down
51 changes: 51 additions & 0 deletions client/app/pages/users/UserProfile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { react2angular } from 'react2angular';

import { EmailSettingsWarning } from '@/components/EmailSettingsWarning';
import UserEdit from '@/components/users/UserEdit';
import UserShow from '@/components/users/UserShow';
import LoadingState from '@/components/items-list/components/LoadingState';

import { User } from '@/services/user';
import settingsMenu from '@/services/settingsMenu';
import { $route } from '@/services/ng';
import { currentUser } from '@/services/auth';
import './settings.less';

class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = { user: null };
}

componentDidMount() {
const userId = $route.current.params.userId || currentUser.id;
User.get({ id: userId }, user => this.setState({ user: User.convertUserInfo(user) }));
}

render() {
const { user } = this.state;
const canEdit = user && (currentUser.isAdmin || currentUser.id === user.id);
const UserComponent = canEdit ? UserEdit : UserShow;
return (
<React.Fragment>
<EmailSettingsWarning featureName="invite emails" />
<div className="row">
{user ? <UserComponent user={user} /> : <LoadingState className="" />}
</div>
</React.Fragment>
);
}
}

export default function init(ngModule) {
settingsMenu.add({
title: 'Account',
path: 'users/me',
order: 7,
});

ngModule.component('pageUserProfile', react2angular(UserProfile));
}

init.init = true;
Loading