feat: Add ability to edit users

Closes #60
This commit is contained in:
Maksim Eltyshev
2022-06-15 14:13:22 +02:00
parent b810dcced7
commit dd83278c83
30 changed files with 775 additions and 204 deletions

View File

@@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Header, Tab } from 'semantic-ui-react';
import InformationEdit from './InformationEdit';
import AvatarEditPopup from './AvatarEditPopup';
import UsernameEditPopup from './UsernameEditPopup';
import EmailEditPopup from './EmailEditPopup';
import PasswordEditPopup from './PasswordEditPopup';
import User from '../../User';
import UserInformationEdit from '../../UserInformationEdit';
import UserUsernameEditPopup from '../../UserUsernameEditPopup';
import UserEmailEditPopup from '../../UserEmailEditPopup';
import UserPasswordEditPopup from '../../UserPasswordEditPopup';
import styles from './AccountPane.module.scss';
@@ -52,7 +52,7 @@ const AccountPane = React.memo(
</AvatarEditPopup>
<br />
<br />
<InformationEdit
<UserInformationEdit
defaultData={{
name,
phone,
@@ -68,7 +68,8 @@ const AccountPane = React.memo(
</Header>
</Divider>
<div className={styles.action}>
<UsernameEditPopup
<UserUsernameEditPopup
usePasswordConfirmation
defaultData={usernameUpdateForm.data}
username={username}
isSubmitting={usernameUpdateForm.isSubmitting}
@@ -81,10 +82,11 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</UsernameEditPopup>
</UserUsernameEditPopup>
</div>
<div className={styles.action}>
<EmailEditPopup
<UserEmailEditPopup
usePasswordConfirmation
defaultData={emailUpdateForm.data}
email={email}
isSubmitting={emailUpdateForm.isSubmitting}
@@ -97,10 +99,11 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</EmailEditPopup>
</UserEmailEditPopup>
</div>
<div className={styles.action}>
<PasswordEditPopup
<UserPasswordEditPopup
usePasswordConfirmation
defaultData={passwordUpdateForm.data}
isSubmitting={passwordUpdateForm.isSubmitting}
error={passwordUpdateForm.error}
@@ -112,7 +115,7 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</PasswordEditPopup>
</UserPasswordEditPopup>
</div>
</Tab.Pane>
);

View File

@@ -1,177 +0,0 @@
import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm } from '../../../hooks';
import styles from './EmailEditPopup.module.scss';
const createMessage = (error) => {
if (!error) {
return error;
}
switch (error.message) {
case 'Email already in use':
return {
type: 'error',
content: 'common.emailAlreadyInUse',
};
case 'Invalid current password':
return {
type: 'error',
content: 'common.invalidCurrentPassword',
};
default:
return {
type: 'warning',
content: 'common.unknownError',
};
}
};
const EmailEditStep = React.memo(
({ defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
const [data, handleFieldChange, setData] = useForm({
email: '',
currentPassword: '',
...defaultData,
});
const message = useMemo(() => createMessage(error), [error]);
const [focusCurrentPasswordFieldState, focusCurrentPasswordField] = useToggle();
const emailField = useRef(null);
const currentPasswordField = useRef(null);
const handleSubmit = useCallback(() => {
const cleanData = {
...data,
email: data.email.trim(),
};
if (!isEmail(cleanData.email)) {
emailField.current.select();
return;
}
if (cleanData.email === email) {
onClose();
return;
}
if (!cleanData.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(cleanData);
}, [email, onUpdate, onClose, data]);
useEffect(() => {
emailField.current.select();
}, []);
useEffect(() => {
if (wasSubmitting && !isSubmitting) {
if (error) {
switch (error.message) {
case 'Email already in use':
emailField.current.select();
break;
case 'Invalid current password':
setData((prevData) => ({
...prevData,
currentPassword: '',
}));
focusCurrentPasswordField();
break;
default:
}
} else {
onClose();
}
}
}, [isSubmitting, wasSubmitting, error, onClose, setData, focusCurrentPasswordField]);
useDidUpdate(() => {
currentPasswordField.current.focus();
}, [focusCurrentPasswordFieldState]);
return (
<>
<Popup.Header>
{t('common.editEmail', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
{message && (
<Message
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
[message.type]: true,
}}
visible
content={t(message.content)}
onDismiss={onMessageDismiss}
/>
)}
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.newEmail')}</div>
<Input
fluid
ref={emailField}
name="email"
value={data.email}
placeholder={email}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
<Button
positive
content={t('action.save')}
loading={isSubmitting}
disabled={isSubmitting}
/>
</Form>
</Popup.Content>
</>
);
},
);
EmailEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
email: PropTypes.string.isRequired,
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
EmailEditStep.defaultProps = {
error: undefined,
};
export default withPopup(EmailEditStep);

View File

@@ -1,12 +0,0 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}

View File

@@ -1,80 +0,0 @@
import { dequal } from 'dequal';
import pickBy from 'lodash/pickBy';
import React, { useCallback, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
import { useForm } from '../../../hooks';
import styles from './InformationEdit.module.scss';
const InformationEdit = React.memo(({ defaultData, onUpdate }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
phone: '',
organization: '',
...pickBy(defaultData),
}));
const cleanData = useMemo(
() => ({
...data,
name: data.name.trim(),
phone: data.phone.trim() || null,
organization: data.organization.trim() || null,
}),
[data],
);
const nameField = useRef(null);
const handleSubmit = useCallback(() => {
if (!cleanData.name) {
nameField.current.select();
return;
}
onUpdate(cleanData);
}, [onUpdate, cleanData]);
return (
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.name')}</div>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.phone')}</div>
<Input
fluid
name="phone"
value={data.phone}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.organization')}</div>
<Input
fluid
name="organization"
value={data.organization}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive disabled={dequal(cleanData, defaultData)} content={t('action.save')} />
</Form>
);
});
InformationEdit.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
};
export default InformationEdit;

View File

@@ -1,12 +0,0 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}

View File

@@ -1,149 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm } from '../../../hooks';
import styles from './PasswordEditPopup.module.scss';
const createMessage = (error) => {
if (!error) {
return error;
}
switch (error.message) {
case 'Invalid current password':
return {
type: 'error',
content: 'common.invalidCurrentPassword',
};
default:
return {
type: 'warning',
content: 'common.unknownError',
};
}
};
const PasswordEditStep = React.memo(
({ defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
const [data, handleFieldChange, setData] = useForm({
password: '',
currentPassword: '',
...defaultData,
});
const message = useMemo(() => createMessage(error), [error]);
const [focusCurrentPasswordFieldState, focusCurrentPasswordField] = useToggle();
const passwordField = useRef(null);
const currentPasswordField = useRef(null);
const handleSubmit = useCallback(() => {
if (!data.password) {
passwordField.current.select();
return;
}
if (!data.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(data);
}, [onUpdate, data]);
useEffect(() => {
passwordField.current.select();
}, []);
useEffect(() => {
if (wasSubmitting && !isSubmitting) {
if (!error) {
onClose();
} else if (error.message === 'Invalid current password') {
setData((prevData) => ({
...prevData,
currentPassword: '',
}));
focusCurrentPasswordField();
}
}
}, [isSubmitting, wasSubmitting, error, onClose, setData, focusCurrentPasswordField]);
useDidUpdate(() => {
currentPasswordField.current.focus();
}, [focusCurrentPasswordFieldState]);
return (
<>
<Popup.Header>
{t('common.editPassword', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
{message && (
<Message
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
[message.type]: true,
}}
visible
content={t(message.content)}
onDismiss={onMessageDismiss}
/>
)}
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.newPassword')}</div>
<Input.Password
fluid
ref={passwordField}
name="password"
value={data.password}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
<Button
positive
content={t('action.save')}
loading={isSubmitting}
disabled={isSubmitting}
/>
</Form>
</Popup.Content>
</>
);
},
);
PasswordEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
PasswordEditStep.defaultProps = {
error: undefined,
};
export default withPopup(PasswordEditStep);

View File

@@ -1,12 +0,0 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}

View File

@@ -1,178 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm } from '../../../hooks';
import { isUsername } from '../../../utils/validator';
import styles from './UsernameEditPopup.module.scss';
const createMessage = (error) => {
if (!error) {
return error;
}
switch (error.message) {
case 'Username already in use':
return {
type: 'error',
content: 'common.usernameAlreadyInUse',
};
case 'Invalid current password':
return {
type: 'error',
content: 'common.invalidCurrentPassword',
};
default:
return {
type: 'warning',
content: 'common.unknownError',
};
}
};
const UsernameEditStep = React.memo(
({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
const [data, handleFieldChange, setData] = useForm({
username: '',
currentPassword: '',
...defaultData,
});
const message = useMemo(() => createMessage(error), [error]);
const [focusCurrentPasswordFieldState, focusCurrentPasswordField] = useToggle();
const usernameField = useRef(null);
const currentPasswordField = useRef(null);
const handleSubmit = useCallback(() => {
const cleanData = {
...data,
username: data.username.trim() || null,
};
if (cleanData.username && !isUsername(cleanData.username)) {
usernameField.current.select();
return;
}
if (cleanData.username === username) {
onClose();
return;
}
if (!cleanData.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(cleanData);
}, [username, onUpdate, onClose, data]);
useEffect(() => {
usernameField.current.select();
}, []);
useEffect(() => {
if (wasSubmitting && !isSubmitting) {
if (error) {
switch (error.message) {
case 'Username already in use':
usernameField.current.select();
break;
case 'Invalid current password':
setData((prevData) => ({
...prevData,
currentPassword: '',
}));
focusCurrentPasswordField();
break;
default:
}
} else {
onClose();
}
}
}, [isSubmitting, wasSubmitting, error, onClose, setData, focusCurrentPasswordField]);
useDidUpdate(() => {
currentPasswordField.current.focus();
}, [focusCurrentPasswordFieldState]);
return (
<>
<Popup.Header>
{t('common.editUsername', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
{message && (
<Message
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
[message.type]: true,
}}
visible
content={t(message.content)}
onDismiss={onMessageDismiss}
/>
)}
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.newUsername')}</div>
<Input
fluid
ref={usernameField}
name="username"
value={data.username}
placeholder={username}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
<Button
positive
content={t('action.save')}
loading={isSubmitting}
disabled={isSubmitting}
/>
</Form>
</Popup.Content>
</>
);
},
);
UsernameEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
username: PropTypes.string,
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
UsernameEditStep.defaultProps = {
username: undefined,
error: undefined,
};
export default withPopup(UsernameEditStep);

View File

@@ -1,12 +0,0 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}