Skip to content

Commit

Permalink
Merge pull request #50 from linkernetworks/phstsai/profile
Browse files Browse the repository at this point in the history
[Task] User profile page, and password changing function
  • Loading branch information
LucienLee authored Sep 26, 2018
2 parents d9c43dd + 95f6632 commit 1e52c49
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 10 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2452,3 +2452,7 @@ In the future, we might start automatically compiling incompatible third-party m
## Something Missing?
If you have ideas for more “How To” recipes that should be on this page, [let us know](https://github.com/facebookincubator/create-react-app/issues) or [contribute some!](https://github.com/facebookincubator/create-react-app/edit/master/packages/react-scripts/template/README.md)
## Acknowledge
The user icon in profile page is made by Freepik from www.flaticon.co .
40 changes: 40 additions & 0 deletions src/assets/user.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 5 additions & 8 deletions src/components/NavHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,12 @@ class NavHeader extends React.Component<NavHeaderProps> {

const menu = (
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
<Menu.Item disabled={true}>
<Icon type="info-circle-o" />
<span>{currentUser && currentUser.role}</span>
{/* <FormattedMessage id="nav.profile" /> */}
<Menu.Item key="profile">
<Link to="/profile">
<Icon type="setting" style={{ marginRight: '8px' }} />
<FormattedMessage id="nav.profile" />
</Link>
</Menu.Item>
{/* <Menu.Item disabled={true}>
<Icon type="setting" />
<FormattedMessage id="nav.setting" />
</Menu.Item> */}
<Menu.Divider />
<Menu.Item key="logout">
<Icon type="logout" />
Expand Down
91 changes: 91 additions & 0 deletions src/components/PasswordForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { compose } from 'recompose';
import { Form, Modal, Input } from 'antd';
import { FormComponentProps } from 'antd/lib/form';

const FormItem = Form.Item;

const formItemLayout = {
labelCol: { span: 8 },
wrapperCol: { span: 14 }
};

type PasswordFormProps = OwnProps & FormComponentProps;

interface OwnProps {
visible: boolean;
username: string;
onCancel: () => void;
onSubmit: (data: any) => void;
}

class PasswordForm extends React.PureComponent<PasswordFormProps, object> {
protected handleSubmit = () => {
this.props.form.validateFields((err, values) => {
if (!err) {
this.props.onSubmit({
username: this.props.username,
password: values.password
});
}
});
};

protected handleClose = () => {
this.props.form.resetFields();
this.props.onCancel();
};

public compareToFirstPassword = (_: any, value: string, callback: any) => {
const form = this.props.form;
if (value && value !== form.getFieldValue('password')) {
callback('Two passwords that you enter is inconsistent!');
} else {
callback();
}
};

public render() {
const { getFieldDecorator } = this.props.form;

return (
<Modal
visible={this.props.visible}
title={<FormattedMessage id="user.changePassword" />}
onOk={this.handleSubmit}
onCancel={this.handleClose}
>
<Form>
<FormItem {...formItemLayout} label="Password">
{getFieldDecorator('password', {
rules: [
{
required: true,
message: 'Please input your password!'
}
]
})(<Input type="password" />)}
</FormItem>
<FormItem {...formItemLayout} label="Confirm Password">
{getFieldDecorator('confirm', {
rules: [
{
required: true,
message: 'Please confirm your password!'
},
{
validator: this.compareToFirstPassword
}
]
})(<Input type="password" />)}
</FormItem>
</Form>
</Modal>
);
}
}

export default compose<PasswordFormProps, OwnProps>(Form.create())(
PasswordForm
);
5 changes: 4 additions & 1 deletion src/locales/en-US/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ export default {
'user.username': 'username',
'user.role': 'role',
'user.phoneNumber': 'phone number',
'user.add': 'Create user'
'user.add': 'Create user',
'user.changePassword': 'Change Password',
'user.newPassword': 'new password',
'user.confirmPassword': 'confirm passowrd'
}
5 changes: 4 additions & 1 deletion src/locales/zh-Hant/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ export default {
'user.username': '使用者名稱',
'user.role': '角色',
'user.phoneNumber': '手機號碼',
'user.add': '新增使用者'
'user.add': '新增使用者',
'user.changePassword': '變更密碼',
'user.newPassword': '新密碼',
'user.confirmPassword': '確認密碼'
}
129 changes: 129 additions & 0 deletions src/routes/Profile/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Card, Table, Icon, Avatar } from 'antd';
import { InjectedIntlProps } from 'react-intl';
import { ColumnProps } from 'antd/lib/table';
import { Dispatch } from 'redux';
import { InjectedAuthRouterProps } from 'redux-auth-wrapper/history4/redirect';
import { userOperations } from '@/store/ducks/user';
import * as styles from './styles.module.scss';
import PasswordForm from '@/components/PasswordForm';
import user from '@/assets/user.svg';

import { RootState, RootAction, RTDispatch } from '@/store/ducks';
import { FlattenUser, User } from '@/models/User';
import { userModels } from '@/store/ducks/user';

type UsersProps = OwnProps & InjectedAuthRouterProps & InjectedIntlProps;

interface OwnProps extends ColumnProps<FlattenUser> {
user: User;
changePassword: (data: userModels.LoginCredential) => any;
}

interface UserState {
visibleModal: boolean;
}

class Profile extends React.PureComponent<UsersProps, UserState> {
constructor(props: UsersProps) {
super(props);
this.state = {
visibleModal: false
};
}

private columns = [
{
dataIndex: 'key'
},
{
dataIndex: 'value'
}
];
private data = [
{
key: 'User Name',
value: this.props.user.loginCredential.username
},
{
key: 'Role',
value: this.props.user.role
},
{
key: 'First Name',
value: this.props.user.firstName
},
{
key: 'Last Name',
value: this.props.user.lastName
},
{
key: 'Phone Number',
value: this.props.user.phoneNumber
}
];

protected showCreate = () => {
this.setState({ visibleModal: true });
};

protected hideCreate = () => {
this.setState({ visibleModal: false });
};

protected handleSubmit = (data: userModels.LoginCredential) => {
this.props.changePassword(data);
this.setState({ visibleModal: false });
};

public render() {
const { Meta } = Card;

return (
<div className={styles.test}>
<Card
style={{ width: 500 }}
actions={[<Icon key="edit" type="edit" onClick={this.showCreate} />]}
>
<Meta
avatar={<Avatar src={user} />}
title={this.props.user.displayName}
description={
<Table
rowKey="name"
columns={this.columns}
dataSource={this.data}
pagination={false}
showHeader={false}
/>
}
/>
</Card>
<PasswordForm
username={this.props.user.loginCredential.username}
visible={this.state.visibleModal}
onCancel={this.hideCreate}
onSubmit={this.handleSubmit}
/>
</div>
);
}
}

const mapStateToProps = (state: RootState) => {
return {
user: state.user.auth.user as User
};
};

const mapDispatchToProps = (dispatch: RTDispatch & Dispatch<RootAction>) => ({
changePassword: (data: userModels.LoginCredential) => {
dispatch(userOperations.changePassword(data));
}
});

export default connect(
mapStateToProps,
mapDispatchToProps
)(Profile);
7 changes: 7 additions & 0 deletions src/routes/Profile/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.test{
transform: translateY(-50%);
top: 35%;
position: relative;
display: flex;
justify-content: center
}
6 changes: 6 additions & 0 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import locationHelperBuilder from 'redux-auth-wrapper/history4/locationHelper';
import { RootState } from '@/store/ducks';
import MainLayout from '@/layouts/MainLayout';
import UserLayout from '@/layouts/UserLayout';
import UserProfile from '@/routes/Profile';
import ClusterOverview from '@/routes/Cluster/Overview';
import Node from '@/routes/Cluster/Node';
import Network from '@/routes/Cluster/Network';
Expand Down Expand Up @@ -71,6 +72,11 @@ const appRoutes = (
/>
<Redirect exact={true} from="/" to="/cluster/overview" />
<Redirect exact={true} from="/cluster" to="/cluster/overview" />
<RouteWithLayout
layout={MainLayout}
component={userIsAuthenticated(UserProfile)}
path="/profile"
/>
<RouteWithLayout
layout={MainLayout}
component={userIsAuthenticated(ClusterOverview)}
Expand Down
6 changes: 6 additions & 0 deletions src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export const signin = (data: LoginCredential): AxiosPromise<Response> => {
return axios.post('/v1/users/signin', data);
};

export const updatePassword = (
data: LoginCredential
): AxiosPromise<Response> => {
return axios.put('/v1/users/password', data);
};

export const getUsers = (): AxiosPromise<Array<User>> => {
return axios.get('/v1/users');
};
Expand Down
6 changes: 6 additions & 0 deletions src/store/ducks/user/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const addUser = createAsyncAction(
'ADD_USER_FAILURE'
)<void, User.User, Error>();

export const changePassword = createAsyncAction(
'CHANGE_PASSWORD_REQUEST',
'CHANGE_PASSWORD_SUCCESS',
'CHANGE_PASSWORD_FAILURE'
)<void, User.LoginCredential, Error>();

export const removeUser = createAsyncAction(
'REMOVE_USER_REQUEST',
'REMOVE_USER_SUCCESS',
Expand Down
18 changes: 18 additions & 0 deletions src/store/ducks/user/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,24 @@ export const removeUser = (id: string): RTAction<Promise<UserActionType>> => {
};
};

export const changePassword = (
data: LoginCredential
): RTAction<Promise<UserActionType>> => {
return async dispatch => {
dispatch(userActions.changePassword.request());
try {
const res = await userAPI.updatePassword(data);
if (!res.data.error) {
return dispatch(userActions.changePassword.success(data));
} else {
throw new Error(res.data.message);
}
} catch (e) {
return dispatch(userActions.changePassword.failure(e.response.data));
}
};
};

export const login = (
data: LoginCredential
): RTAction<Promise<UserActionType>> => {
Expand Down
Loading

0 comments on commit 1e52c49

Please sign in to comment.