Complete account change

This commit is contained in:
Paulo Gustavo Veiga 2021-02-15 15:42:10 -08:00
parent 3a10dce222
commit d9add8605c
7 changed files with 245 additions and 112 deletions

View File

@ -58,8 +58,8 @@ export type ErrorInfo = {
} }
export type AccountInfo = { export type AccountInfo = {
firstName: string; firstname: string;
lastName: string; lastname: string;
email: string; email: string;
locale: Locale; locale: Locale;
} }
@ -75,6 +75,7 @@ interface Client {
updateAccountLanguage(locale: LocaleCode): Promise<void>; updateAccountLanguage(locale: LocaleCode): Promise<void>;
updateAccountPassword(pasword: string): Promise<void>; updateAccountPassword(pasword: string): Promise<void>;
updateAccountInfo(firstname: string,lastname: string): Promise<void>;
updateStarred(id: number, starred: boolean): Promise<void>; updateStarred(id: number, starred: boolean): Promise<void>;
updateMapToPublic(id: number, starred: boolean): Promise<void>; updateMapToPublic(id: number, starred: boolean): Promise<void>;

View File

@ -34,6 +34,9 @@ class MockClient implements Client {
]; ];
} }
updateAccountInfo(firstname: string, lastname: string): Promise<void> {
throw new Error('Method not implemented.');
}
updateAccountPassword(pasword: string): Promise<void> { updateAccountPassword(pasword: string): Promise<void> {
return Promise.resolve(); return Promise.resolve();
@ -52,8 +55,8 @@ class MockClient implements Client {
console.log('Fetch account info ...') console.log('Fetch account info ...')
const locale: LocaleCode | null = localStorage.getItem('locale') as LocaleCode; const locale: LocaleCode | null = localStorage.getItem('locale') as LocaleCode;
return Promise.resolve({ return Promise.resolve({
firstName: 'Costme', firstname: 'Costme',
lastName: 'Fulanito', lastname: 'Fulanito',
email: 'test@example.com', email: 'test@example.com',
locale: localeFromStr(locale) locale: localeFromStr(locale)
}); });

View File

@ -11,6 +11,27 @@ export default class RestClient implements Client {
this.sessionExpired = sessionExpired; this.sessionExpired = sessionExpired;
} }
updateAccountInfo(firstname: string, lastname: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios.put(`${this.baseUrl}/c/restful/account/firstname`,
firstname,
{ headers: { 'Content-Type': 'text/plain' } }
).then(() => {
return axios.put(`${this.baseUrl}/c/restful/account/lastname`,
lastname,
{ headers: { 'Content-Type': 'text/plain' } }
)
}).then(() => {
// All was ok, let's sent to success page ...;
success();
}).catch(error => {
const errorInfo = this.parseResponseOnError(error.response);
reject(errorInfo);
});
}
return new Promise(handler);
}
updateAccountPassword(pasword: string): Promise<void> { updateAccountPassword(pasword: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios.put(`${this.baseUrl}/c/restful/account/password`, axios.put(`${this.baseUrl}/c/restful/account/password`,
@ -23,7 +44,8 @@ export default class RestClient implements Client {
reject(errorInfo); reject(errorInfo);
}); });
} }
return new Promise(handler); } return new Promise(handler);
}
updateAccountLanguage(locale: LocaleCode): Promise<void> { updateAccountLanguage(locale: LocaleCode): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
@ -68,8 +90,8 @@ export default class RestClient implements Client {
const account = response.data; const account = response.data;
const locale: LocaleCode | null = account.locale; const locale: LocaleCode | null = account.locale;
success({ success({
lastName: account.lastName ? account.lastName : '', lastname: account.lastname ? account.lastname : '',
firstName: account.fistName ? account.fistName : '', firstname: account.firstname ? account.firstname : '',
email: account.email, email: account.email,
locale: locale ? localeFromStr(locale) : Locales.EN locale: locale ? localeFromStr(locale) : Locales.EN
}); });

View File

@ -1,6 +1,5 @@
import { TextField } from "@material-ui/core"; import { TextField } from "@material-ui/core";
import React, { ChangeEvent } from "react"; import React, { ChangeEvent } from "react";
import { MessageDescriptor, useIntl } from "react-intl";
import { ErrorInfo } from "../../../classes/client"; import { ErrorInfo } from "../../../classes/client";
type InputProps = { type InputProps = {
@ -13,24 +12,29 @@ type InputProps = {
value?: string value?: string
autoComplete?: string; autoComplete?: string;
fullWidth?: boolean fullWidth?: boolean
disabled?: boolean
} }
const Input = (props: InputProps) => { const Input = ({
name,
error,
onChange,
required = true,
type,
value,
label,
autoComplete,
fullWidth = true,
disabled = false
}: InputProps) => {
const intl = useIntl();
const error: ErrorInfo | undefined = props?.error;
const name = props.name;
const value = props.value;
const onChange = props.onChange ? props.onChange : () => { };
const fieldError = error?.fields?.[name]; const fieldError = error?.fields?.[name];
const required = props.required != undefined ? props.required : true;
const fullWidth = props.fullWidth != undefined ? props.required : true;
return ( return (
<TextField name={name} type={props.type} label={props.label} <TextField name={name} type={type} label={label}
value={value} onChange={onChange} value={value} onChange={onChange}
error={Boolean(fieldError)} helperText={fieldError} error={Boolean(fieldError)} helperText={fieldError}
variant="outlined" required={required} fullWidth={fullWidth} margin="dense"/> variant="outlined" required={required} fullWidth={fullWidth} margin="dense" disabled={disabled} autoComplete={autoComplete} />
); );
} }

View File

@ -0,0 +1,94 @@
import { FormControl } from "@material-ui/core";
import React, { useEffect } from "react";
import { useIntl } from "react-intl";
import { useMutation, useQueryClient } from "react-query";
import Client, { ErrorInfo } from "../../../../classes/client";
import Input from "../../../form/input";
import BaseDialog from "../../action-dispatcher/base-dialog";
import { useSelector } from 'react-redux';
import { activeInstance, fetchAccount } from "../../../../redux/clientSlice";
type AccountInfoDialogProps = {
onClose: () => void
}
type AccountInfoModel = {
email: string,
firstname: string,
lastname: string
}
const defaultModel: AccountInfoModel = { firstname: '', lastname: '', email: '' };
const AccountInfoDialog = ({ onClose }: AccountInfoDialogProps) => {
const client: Client = useSelector(activeInstance);
const queryClient = useQueryClient();
const [model, setModel] = React.useState<AccountInfoModel>(defaultModel);
const [error, setError] = React.useState<ErrorInfo>();
const intl = useIntl();
const mutation = useMutation<void, ErrorInfo, AccountInfoModel>((model: AccountInfoModel) => {
return client.updateAccountInfo(model.firstname, model.lastname);
},
{
onSuccess: () => {
queryClient.invalidateQueries('account')
onClose()
},
onError: (error) => {
setError(error);
}
}
);
const account = fetchAccount();
useEffect(() => {
if (account) {
setModel({
email: account?.email,
lastname: account?.lastname,
firstname: account?.firstname
});
}
}, [account?.email])
const handleOnClose = (): void => {
onClose();
setModel(defaultModel);
setError(undefined);
};
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
mutation.mutate(model);
};
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
event.preventDefault();
const name = event.target.name;
const value = event.target.value;
setModel({ ...model, [name as keyof AccountInfoModel]: value });
}
return (
<BaseDialog onClose={handleOnClose} onSubmit={handleOnSubmit} error={error}
title={intl.formatMessage({ id: 'accountinfo.title', defaultMessage: 'Account info' })}
submitButton={intl.formatMessage({ id: 'accountinfo.button', defaultMessage: 'Save' })}>
<FormControl fullWidth={true}>
<Input name="email" type="text" disabled={true} label={intl.formatMessage({ id: "accountinfo.email", defaultMessage: "Email" })}
value={model.email} onChange={handleOnChange} error={error} fullWidth={true} />
<Input name="firstname" type="text" label={intl.formatMessage({ id: "accountinfo.firstname", defaultMessage: "First Name" })}
value={model.firstname} onChange={handleOnChange} required={true} fullWidth={true} />
<Input name="lastname" type="text" label={intl.formatMessage({ id: "accountinfo.lastname", defaultMessage: "Last Name" })}
value={model.lastname} onChange={handleOnChange} required={true} fullWidth={true} />
</FormControl>
</BaseDialog>
);
}
export default AccountInfoDialog;

View File

@ -0,0 +1,84 @@
import { FormControl } from "@material-ui/core";
import React from "react";
import { useIntl } from "react-intl";
import { useMutation } from "react-query";
import Client, { ErrorInfo } from "../../../../classes/client";
import Input from "../../../form/input";
import BaseDialog from "../../action-dispatcher/base-dialog";
import { useSelector } from 'react-redux';
import { activeInstance } from "../../../../redux/clientSlice";
type ChangePasswordDialogProps = {
onClose: () => void
}
type ChangePasswordModel = {
password: string,
retryPassword: string
}
const defaultModel: ChangePasswordModel = { password: '', retryPassword: '' };
const ChangePasswordDialog = ({ onClose }: ChangePasswordDialogProps) => {
const client: Client = useSelector(activeInstance);
const [model, setModel] = React.useState<ChangePasswordModel>(defaultModel);
const [error, setError] = React.useState<ErrorInfo>();
const intl = useIntl();
const mutation = useMutation<void, ErrorInfo, ChangePasswordModel>((model: ChangePasswordModel) => {
return client.updateAccountPassword(model.password);
},
{
onSuccess: () => {
onClose()
},
onError: (error) => {
setError(error);
}
}
);
const handleOnClose = (): void => {
onClose();
setModel(defaultModel);
setError(undefined);
};
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
// Check password are equal ...
if (model.password != model.retryPassword) {
setError({ msg: intl.formatMessage({ id: 'changepwd.password-match', defaultMessage: 'Password do not match. Please, try again.' }) });
return;
}
mutation.mutate(model);
};
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
event.preventDefault();
const name = event.target.name;
const value = event.target.value;
setModel({ ...model, [name as keyof ChangePasswordModel]: value });
}
return (
<BaseDialog onClose={handleOnClose} onSubmit={handleOnSubmit} error={error}
title={intl.formatMessage({ id: 'changepwd.title', defaultMessage: 'Change Password' })}
description={intl.formatMessage({ id: 'changepwd.description', defaultMessage: 'Please, provide the new password for your account.' })}
submitButton={intl.formatMessage({ id: 'changepwd.button', defaultMessage: 'Change' })}>
<FormControl fullWidth={true}>
<Input name="password" type="password" label={intl.formatMessage({ id: "changepwd.password", defaultMessage: "Password" })}
value={model.password} onChange={handleOnChange} error={error} fullWidth={true} autoComplete="new-password" />
<Input name="retryPassword" type="password" label={intl.formatMessage({ id: "changepwd.confirm-password", defaultMessage: "Confirm Password" })}
value={model.retryPassword} onChange={handleOnChange} required={true} fullWidth={true} autoComplete="new-password" />
</FormControl>
</BaseDialog>
);
}
export default ChangePasswordDialog;

View File

@ -1,19 +1,16 @@
import { FormControl, IconButton, Link, ListItemIcon, Menu, MenuItem, Tooltip } from '@material-ui/core'; import { IconButton, Link, ListItemIcon, Menu, MenuItem, Tooltip } from '@material-ui/core';
import { AccountCircle, ExitToAppOutlined, LockOpenOutlined, SettingsApplicationsOutlined } from '@material-ui/icons'; import { AccountCircle, ExitToAppOutlined, LockOpenOutlined, SettingsApplicationsOutlined } from '@material-ui/icons';
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useMutation } from "react-query"; import { fetchAccount } from '../../../redux/clientSlice';
import Client, { ErrorInfo } from "../../../classes/client"; import AccountInfoDialog from './account-info-dialog';
import { useSelector } from 'react-redux'; import ChangePasswordDialog from './change-password-dialog';
import { activeInstance, fetchAccount } from '../../../redux/clientSlice';
import BaseDialog from '../action-dispatcher/base-dialog';
import Input from '../../form/input';
type ActionType = 'change-password' | 'account-info' | undefined;
const AccountMenu = () => { const AccountMenu = () => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const [openRenameDialog, setOpenRenameDialog] = React.useState<boolean>(false); const [action, setAction] = React.useState<ActionType>(undefined);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => { const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
@ -27,7 +24,7 @@ const AccountMenu = () => {
const account = fetchAccount(); const account = fetchAccount();
return ( return (
<span> <span>
<Tooltip title={`${account?.firstName} ${account?.lastName} <${account?.email}>`}> <Tooltip title={`${account?.firstname} ${account?.lastname} <${account?.email}>`}>
<IconButton <IconButton
onClick={handleMenu}> onClick={handleMenu}>
<AccountCircle fontSize="large" style={{ color: 'black' }} /> <AccountCircle fontSize="large" style={{ color: 'black' }} />
@ -48,18 +45,18 @@ const AccountMenu = () => {
horizontal: 'right', horizontal: 'right',
}} }}
> >
<MenuItem onClick={handleClose}> <MenuItem onClick={() => { handleClose(), setAction('account-info') }}>
<ListItemIcon> <ListItemIcon>
<SettingsApplicationsOutlined fontSize="small" /> <SettingsApplicationsOutlined fontSize="small" />
</ListItemIcon> </ListItemIcon>
<FormattedMessage id="menu.account" defaultMessage="Account" /> <FormattedMessage id="menu.account" defaultMessage="Account" />
</MenuItem> </MenuItem>
<MenuItem onClick={() => { handleClose(), setOpenRenameDialog(true) }}> <MenuItem onClick={() => { handleClose(), setAction('change-password') }}>
<ListItemIcon> <ListItemIcon>
<LockOpenOutlined fontSize="small" /> <LockOpenOutlined fontSize="small" />
</ListItemIcon> </ListItemIcon>
<FormattedMessage id="menu.change-password" defaultMessage="Change Password" /> <FormattedMessage id="menu.change-password" defaultMessage="Change password" />
</MenuItem> </MenuItem>
<MenuItem onClick={handleClose}> <MenuItem onClick={handleClose}>
@ -71,87 +68,15 @@ const AccountMenu = () => {
</Link> </Link>
</MenuItem> </MenuItem>
</Menu> </Menu>
{openRenameDialog && {action == 'change-password' &&
<ChangePasswordDialog onClose={() => setOpenRenameDialog(false)} /> <ChangePasswordDialog onClose={() => setAction(undefined)} />
}
{action == 'account-info' &&
<AccountInfoDialog onClose={() => setAction(undefined)} />
} }
</span>); </span>);
} }
type ChangePasswordDialogProps = {
onClose: () => void
}
type ChangePasswordModel = {
password: string,
retryPassword: string
}
const defaultModel: ChangePasswordModel = { password: '', retryPassword: '' };
const ChangePasswordDialog = ({ onClose }: ChangePasswordDialogProps) => {
const client: Client = useSelector(activeInstance);
const [model, setModel] = React.useState<ChangePasswordModel>(defaultModel);
const [error, setError] = React.useState<ErrorInfo>();
const intl = useIntl();
const mutation = useMutation<void, ErrorInfo, ChangePasswordModel>((model: ChangePasswordModel) => {
return client.updateAccountPassword(model.password);
},
{
onSuccess: () => {
onClose()
},
onError: (error) => {
setError(error);
}
}
);
const handleOnClose = (): void => {
onClose();
setModel(defaultModel);
setError(undefined);
};
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
// Check password are equal ...
if (model.password != model.retryPassword) {
setError({ msg: intl.formatMessage({ id: 'changepwd.password-match', defaultMessage: 'Password do not match. Please, try again.' }) });
return;
}
mutation.mutate(model);
};
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
event.preventDefault();
const name = event.target.name;
const value = event.target.value;
setModel({ ...model, [name as keyof ChangePasswordModel]: value });
}
return (
<div>
<BaseDialog onClose={handleOnClose} onSubmit={handleOnSubmit} error={error}
title={intl.formatMessage({ id: 'changepwd.title', defaultMessage: 'Change Password' })}
description={intl.formatMessage({ id: 'changepwd.description', defaultMessage: 'Please, provide the new password for your account.' })}
submitButton={intl.formatMessage({ id: 'changepwd.button', defaultMessage: 'Change' })}>
<FormControl fullWidth={true}>
<Input name="password" type="password" label={intl.formatMessage({ id: "changepwd.password", defaultMessage: "Password" })}
value={model.password} onChange={handleOnChange} error={error} fullWidth={true} autoComplete="new-password" />
<Input name="retryPassword" type="password" label={intl.formatMessage({ id: "changepwd.confirm-password", defaultMessage: "Confirm Password" })}
value={model.retryPassword} onChange={handleOnChange} required={true} fullWidth={true} autoComplete="new-password" />
</FormControl>
</BaseDialog>
</div>
);
}
export default AccountMenu; export default AccountMenu;