Commit e58b4782 authored by Maxym Borodenko's avatar Maxym Borodenko
Browse files

Dashboard: User profile section

parent f98623ae
......@@ -52,11 +52,17 @@
},
"dashboard": {
"title": "My Dashboard",
"subtitle": "Manage data published on Genesys"
},
"userprofile": {
"title": "User profile",
"subtitle": "You can see and update your profile information here"
"subtitle": "Manage data published on Genesys",
"profile": {
"title": "User profile",
"password": {
"disable to change pass": "You can't set your password if you use Google auth",
"title": "Password change",
"New password": "New password",
"Old password": "Old password",
"Confirm password": "Confirm password"
}
}
}
},
"stats": {
......
import {MY_LIST_ACCESSION_ADD, MY_LIST_ACCESSION_INIT, MY_LIST_ACCESSION_REMOVE} from 'constants/user';
import {MY_LIST_ACCESSION_ADD, MY_LIST_ACCESSION_INIT, MY_LIST_ACCESSION_REMOVE, RECEIVE_USER_PROFILE} from 'constants/user';
import {User} from 'model/User';
import {log} from 'utilities/debug';
import {UserProfileService} from 'service/UserProfileService';
import {showSnackbar} from './snackbar';
import {SubmissionError} from 'redux-form';
import * as _ from 'lodash';
const initAccessions = (accessions: string[]) => ({
type: MY_LIST_ACCESSION_INIT,
......@@ -15,6 +21,10 @@ const removeAccession = (accUUID: string) => ({
payload: accUUID,
});
const receiveUserProfile = (user: User) => ({
type: RECEIVE_USER_PROFILE, payload: user,
});
export const addAccessionToMyList = (accUUID: string) => (dispatch, getState) => {
const myList = getState().user.myList;
if (!myList.accessions.includes(accUUID)) {
......@@ -43,3 +53,27 @@ export const initMyList = () => (dispatch) => {
}
}
};
export const loadUserProfile = () => (dispatch, getState) => {
const token = getState().login.access_token;
return UserProfileService.getProfile(token)
// receive the current user profile
.then((userProfile) => {
return dispatch(receiveUserProfile(userProfile));
})
.catch((error) => {
log('Error', error);
});
};
export const changePassword = (newPassword: string, oldPassword: string) => (dispatch, getState) => {
return UserProfileService.changePassword(getState().login.access_token, newPassword, oldPassword)
.then(() => {
dispatch(showSnackbar('Password was changed successfully'));
}).catch((error) => {
const data = _.get(error, 'response.data');
log('Save error', data.error);
dispatch(showSnackbar(data.error || 'Password change error'));
throw new SubmissionError({ title: 'Password change error', _error: data.error });
});
};
......@@ -5,11 +5,12 @@ const origin = typeof window !== 'undefined' ?
: 'http://localhost:3000';
export const API_ROOT = `${origin}/proxy`;
export const API_BASE_URL = `${API_ROOT}/api/v0`;
export const SERVER_INFO_URL = `${API_BASE_URL}/info/version`;
export const LOGIN_URL = `${API_ROOT}/oauth/token`;
export const LOGOUT_URL = `${API_ROOT}/api/v0/me/logout`;
export const LOGOUT_URL = `${API_BASE_URL}/me/logout`;
export const GET_USER_PROFILE_URL = `${API_BASE_URL}/me/profile`;
export const CHANGE_USER_PASSWORD_URL = `${API_BASE_URL}/me/password`;
export const CHECK_TOKEN_URL = `${API_ROOT}/oauth/check_token`;
export const VERIFY_GOOGLE_TOKEN_URL = `${API_ROOT}/google/verify-token`;
export const API_BASE_URL = `${API_ROOT}/api/v0`;
export const SERVER_INFO_URL = `${API_ROOT}/api/v1/info/version`;
export const MY_LIST_ACCESSION_INIT = 'MY_LIST_ACCESSION_INIT';
export const MY_LIST_ACCESSION_ADD = 'MY_LIST_ACCESSION_ADD';
export const MY_LIST_ACCESSION_REMOVE = 'MY_LIST_ACCESSION_REMOVE';
export const RECEIVE_USER_PROFILE = 'RECEIVE_USER_PROFILE';
export const CHANGE_PASSWORD_FORM = 'Form/User/CHANGE_PASSWORD_FORM';
import { UuidModel } from 'model/common.model';
class User extends UuidModel {
public clazz: string = 'org.genesys2.server.model.impl.User';
public accountExpired: boolean;
public accountLocked: boolean;
public accountNonExpired: boolean;
public accountNonLocked: boolean;
public email: string;
public accountType: AccountType;
public roles: UserRole[];
public shortName: string;
public fullName: string;
public accountExpires: Date;
public lockedUntil: Date;
public passwordExpires: Date;
public constructor(obj?) {
super(obj);
}
}
enum UserRole {
USER = 'USER',
ADMINISTRATOR = 'ADMINISTRATOR',
EVERYONE = 'EVERYONE',
}
enum AccountType {
LOCAL = 'LOCAL',
GOOGLE = 'GOOGLE',
SYSTEM = 'SYSTEM',
}
export { User, UserRole, AccountType };
import {MY_LIST_ACCESSION_ADD, MY_LIST_ACCESSION_INIT, MY_LIST_ACCESSION_REMOVE} from 'constants/user';
import {MY_LIST_ACCESSION_ADD, MY_LIST_ACCESSION_INIT, MY_LIST_ACCESSION_REMOVE, RECEIVE_USER_PROFILE} from 'constants/user';
import {COLLAPSE_SIDEBAR} from 'constants/layout';
import update from 'immutability-helper';
import {User} from 'model/User';
const INITIAL_STATE: {
myList: any;
sidebarOpen: boolean;
userProfile: User;
} = {
myList: {
count: 0,
accessions: [],
},
sidebarOpen: true,
userProfile: null,
};
export default function user(state = INITIAL_STATE, action: { type?: string, payload?: any } = {type: '', payload: {}}) {
......@@ -46,6 +49,11 @@ export default function user(state = INITIAL_STATE, action: { type?: string, pay
sidebarOpen: {$set: action.payload},
});
}
case RECEIVE_USER_PROFILE: {
return update(state, {
userProfile: { $set: action.payload },
});
}
default:
return state;
}
......
import { GET_USER_PROFILE_URL, CHANGE_USER_PASSWORD_URL } from 'constants/apiURLS';
import {log} from 'utilities/debug';
import authenticatedRequest from 'utilities/requestUtils';
import {User} from 'model/User';
export class UserProfileService {
// Get current user profile
public static getProfile(token: string): Promise<User> {
log('Loading current user profile.');
return authenticatedRequest(token, {
url: `${GET_USER_PROFILE_URL}`,
method: 'GET',
}).then(({data}) => new User(data));
}
// Change user password
public static changePassword(token: string, newPassword: string, oldPassword: string): Promise<any> {
log('Changing user password');
const data = new FormData();
data.append('old', oldPassword);
data.append('new', newPassword);
return authenticatedRequest(token, {
url: `${CHANGE_USER_PASSWORD_URL}`,
method: 'POST',
data,
});
}
}
import * as React from 'react';
import Card, {CardHeader, CardContent} from 'ui/common/Card';
import { Properties, PropertiesItem } from 'ui/common/Properties';
import PrettyDate from 'ui/common/time/PrettyDate';
import {User} from 'model/User';
import Grid from '@material-ui/core/Grid';
const UserProfileCard = ({userProfile, ...other}: { userProfile: User } & React.ClassAttributes<any>) => {
return (
<div className="">
<Card square>
<CardHeader title={ userProfile.fullName } />
<CardContent>
<Grid container spacing={ 0 }>
<Grid item xs={ 12 }>
<Properties>
<PropertiesItem title="Full name:"> { userProfile.fullName || <i>Not specified</i> }</PropertiesItem>
<PropertiesItem title="E-mail address:"> { userProfile.email }</PropertiesItem>
<PropertiesItem title="Account type:"> { userProfile.accountType }</PropertiesItem>
<PropertiesItem title="Roles:"> {
userProfile.roles.map((role) => (<div style={ {width: '100%'} } key={ role }>{ role }</div>))
} </PropertiesItem>
<PropertiesItem title="Account expires:"><PrettyDate value={ userProfile.accountExpires } /></PropertiesItem>
</Properties>
</Grid>
</Grid>
</CardContent>
</Card>
</div>
);
};
export default UserProfileCard;
import * as React from 'react';
// import DescriptorIcon from '@material-ui/icons/Description';
import DescriptorListIcon from '@material-ui/icons/List';
import AccountBoxIcon from '@material-ui/icons/AccountBox';
import PageLayout from './PageLayout';
import MenuItem from 'ui/common/Menu';
......@@ -9,17 +10,21 @@ import renderRoutes from 'ui/renderRoutes';
const DashboardMenu = () => (
<div>
<MenuItem label="Subsets" to="/dashboard/subsets" icon={ <DescriptorListIcon/> }>
<MenuItem label="List subsets" to="/dashboard/subsets"/>
<MenuItem label="Create subset" to="/subsets/edit" />
</MenuItem>
<MenuItem label="Profile" to="/dashboard/profile" icon={ <AccountBoxIcon/> }>
<MenuItem label="User profile" to="/dashboard/profile"/>
<MenuItem label="Change password" to="/dashboard/profile/password" />
</MenuItem>
<MenuItem label="Subsets" to="/dashboard/subsets" icon={ <DescriptorListIcon/> }>
<MenuItem label="List subsets" to="/dashboard/subsets"/>
<MenuItem label="Create subset" to="/subsets/edit" />
</MenuItem>
</div>
);
const Layout = ({route, match}: { route: any, match: any}) => (
<PageLayout sidebar={ <DashboardMenu /> }>
{ renderRoutes(route.routes, match.path) }
</PageLayout>
<PageLayout sidebar={ <DashboardMenu /> }>
{ renderRoutes(route.routes, match.path) }
</PageLayout>
);
export default Layout;
......@@ -115,7 +115,6 @@ class UserMenuComponent extends React.Component<IUserMenuComponentProps, any> {
<Link to="/admin/"><MenuItem>{ t('menu.Admin') }</MenuItem></Link>
</Authorize>
<Link to="/dashboard"><MenuItem>{ t('menu.My Dashboard') }</MenuItem></Link>
<Link to="/profile"><MenuItem>{ t('menu.My profile') }</MenuItem></Link>
<Link to="/a"><MenuItem>{ t('menu.Accessions') }</MenuItem></Link>
<MenuItem onClick={ logoutRequest } >{ t('common:action.logout') }</MenuItem>
</Menu>
......
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';
import { setPageTitle } from 'actions/pageTitle';
import { changePassword, loadUserProfile } from 'actions/user';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import Card, { CardContent} from 'ui/common/Card';
import Loading from 'ui/common/Loading';
import PasswordForm from './c/PasswordForm';
import {AccountType, User} from 'model/User';
interface IChangePasswordProps extends React.ClassAttributes<any> {
setPageTitle: (title: string) => void;
title: string;
changePassword: (newPassword: string, oldPassword: string) => void;
loadUserProfile: () => void;
userProfile: User;
t: any;
}
class ChangePasswordPage extends React.Component<IChangePasswordProps, void> {
public static needs = [
() => loadUserProfile(),
];
constructor(props) {
super(props);
const { setPageTitle, title} = this.props;
setPageTitle(title);
}
public componentWillMount() {
const { loadUserProfile, userProfile } = this.props;
if (!userProfile) {
loadUserProfile();
}
}
private onChangePassword = (values) => {
const { changePassword } = this.props;
return changePassword(values.newPassword, values.oldPassword);
}
public render() {
const { userProfile, t } = this.props;
const stillLoading: boolean = (! userProfile);
return (
<div>
{ stillLoading ? <Loading /> :
<div>
{ userProfile.accountType === AccountType.LOCAL ?
<div>
<ContentHeaderWithButton title={ t('p.dashboard.profile.password.title') } />
<Card>
<CardContent>
<PasswordForm t={ t } onSubmit={ this.onChangePassword } />
</CardContent>
</Card>
</div>
: <ContentHeaderWithButton title={ t('p.dashboard.profile.password.disable to change pass') } />
}
</div>
}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
title: ownProps.route.extraProps.title, // route-configured
userProfile: state.user.userProfile,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
setPageTitle,
changePassword,
loadUserProfile,
}, dispatch);
export default translate()(connect(mapStateToProps, mapDispatchToProps)(ChangePasswordPage));
import * as React from 'react';
import {CHANGE_PASSWORD_FORM} from 'constants/user';
import {Field, reduxForm} from 'redux-form';
import Button from '@material-ui/core/Button';
import Validators from 'utilities/Validators';
import {TextField} from 'ui/common/text-field';
class PasswordForm extends React.Component<any, void> {
public constructor(props: any) {
super(props);
}
public render() {
const { t, error, handleSubmit, submitting } = this.props;
return (
<form onSubmit={ handleSubmit }>
<Field
name="oldPassword"
type="password"
label={ t('p.dashboard.profile.password.Old password') }
component={ TextField }
validate={ [ Validators.required ] }
/>
<Field
name="newPassword"
type="password"
label={ t('p.dashboard.profile.password.New password') }
component={ TextField }
validate={ [ Validators.required ] }
/>
<Field
name="confirmPassword"
type="password"
label={ t('p.dashboard.profile.password.Confirm password') }
component={ TextField }
validate={ [ Validators.required ] }
/>
{ error && <div style={ { color: 'red' } }>{ error }</div> }
<Button variant="raised" type="submit" style={ { margin: '1rem 0 1rem 0' } } disabled={ submitting }>
Change password
</Button>
</form>
);
}
}
const validate = (values) => {
if (values.newPassword && values.confirmPassword) {
if (values.newPassword !== values.confirmPassword) {
return { confirmPassword : 'New passwords do not match.' };
}
}
return {};
};
export default reduxForm({
form: CHANGE_PASSWORD_FORM,
enableReinitialize: true, validate,
})(PasswordForm);
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';
import { setPageTitle } from 'actions/pageTitle';
import { loadUserProfile } from 'actions/user';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import Loading from 'ui/common/Loading';
import UserProfileCard from 'ui/genesys/userProfile/UserProfileCard';
import { User } from 'model/User';
interface IUserProfileProps extends React.ClassAttributes<any> {
setPageTitle: (title: string) => void;
title: string;
loadUserProfile: () => void;
userProfile: User;
t: any;
}
class UserProfile extends React.Component<IUserProfileProps, void> {
public static needs = [
() => loadUserProfile(),
];
constructor(props) {
super(props);
const { setPageTitle, title } = this.props;
setPageTitle(title);
}
public componentWillMount() {
const { loadUserProfile, userProfile } = this.props;
if (!userProfile) {
loadUserProfile();
}
}
public render() {
const { userProfile, t } = this.props;
const stillLoading: boolean = (! userProfile);
return (
<div>
{ stillLoading ? <Loading /> :
<div>
<ContentHeaderWithButton title={ t('p.dashboard.profile.title') } />
<UserProfileCard userProfile={ userProfile } />
</div>
}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
title: ownProps.route.extraProps.title, // route-configured
userProfile: state.user.userProfile,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
setPageTitle,
loadUserProfile,
}, dispatch);
export default translate()(connect(mapStateToProps, mapDispatchToProps)(UserProfile));
......@@ -11,6 +11,8 @@ import LoginPage from 'ui/pages/login/LoginPage';
import SubsetBrowsePage from 'ui/pages/subsets/BrowsePage';
import SubsetDisplayPage from 'ui/pages/subsets/DisplayPage';
import SubsetDashboardPage from 'ui/pages/dashboard/subsets/DashboardPage';
import UserProfilePage from 'ui/pages/dashboard/profile';
import ChangePasswordPage from 'ui/pages/dashboard/profile/ChangePasswordPage';
import SubsetStepper from 'ui/pages/dashboard/subsets/StepperPage';
import BasicInfoStep from 'ui/pages/dashboard/subsets/subset-stepper/steps/basic-info';
import AccessionsListStep from 'ui/pages/dashboard/subsets/subset-stepper/steps/accessions-list';
......@@ -181,6 +183,22 @@ const routes = [
title: 'My Dashboard',
},
},
{
path: '/profile',
component: UserProfilePage,
exact: true,
extraProps: {
title: 'User Profile',
},
},
{
path: '/profile/password',
component: ChangePasswordPage,
exact: true,
extraProps: {
title: 'Change Password',
},
},
{
path: '/:tab?',
component: DashboardPage,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment