diff --git a/client/src/actions/entry/login.js b/client/src/actions/entry/login.js index e5cf041a..7faf5323 100755 --- a/client/src/actions/entry/login.js +++ b/client/src/actions/entry/login.js @@ -7,8 +7,8 @@ export const authenticate = (data) => ({ }, }); -export const clearAuthenticationError = () => ({ - type: EntryActionTypes.AUTHENTICATION_ERROR_CLEAR, +export const clearAuthenticateError = () => ({ + type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, payload: {}, }); diff --git a/client/src/actions/entry/user.js b/client/src/actions/entry/user.js index 29a792f9..8d157958 100755 --- a/client/src/actions/entry/user.js +++ b/client/src/actions/entry/user.js @@ -7,8 +7,8 @@ export const createUser = (data) => ({ }, }); -export const clearUserCreationError = () => ({ - type: EntryActionTypes.USER_CREATION_ERROR_CLEAR, +export const clearUserCreateError = () => ({ + type: EntryActionTypes.USER_CREATE_ERROR_CLEAR, payload: {}, }); @@ -27,6 +27,30 @@ export const updateCurrentUser = (data) => ({ }, }); +export const updateCurrentUserEmail = (data) => ({ + type: EntryActionTypes.CURRENT_USER_EMAIL_UPDATE, + payload: { + data, + }, +}); + +export const clearCurrentUserEmailUpdateError = () => ({ + type: EntryActionTypes.CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR, + payload: {}, +}); + +export const updateCurrentUserPassword = (data) => ({ + type: EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE, + payload: { + data, + }, +}); + +export const clearCurrentUserPasswordUpdateError = () => ({ + type: EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR, + payload: {}, +}); + export const uploadCurrentUserAvatar = (file) => ({ type: EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD, payload: { diff --git a/client/src/actions/login.js b/client/src/actions/login.js index 8c1621fe..e206d3d5 100644 --- a/client/src/actions/login.js +++ b/client/src/actions/login.js @@ -9,8 +9,8 @@ export const authenticate = (data) => ({ }, }); -export const clearAuthenticationError = () => ({ - type: ActionTypes.AUTHENTICATION_ERROR_CLEAR, +export const clearAuthenticateError = () => ({ + type: ActionTypes.AUTHENTICATE_ERROR_CLEAR, payload: {}, }); diff --git a/client/src/actions/user.js b/client/src/actions/user.js index 982550da..c00ccb27 100644 --- a/client/src/actions/user.js +++ b/client/src/actions/user.js @@ -9,8 +9,8 @@ export const createUser = (data) => ({ }, }); -export const clearUserCreationError = () => ({ - type: ActionTypes.USER_CREATION_ERROR_CLEAR, +export const clearUserCreateError = () => ({ + type: ActionTypes.USER_CREATE_ERROR_CLEAR, payload: {}, }); @@ -22,6 +22,20 @@ export const updateUser = (id, data) => ({ }, }); +export const clearUserEmailUpdateError = (id) => ({ + type: ActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR, + payload: { + id, + }, +}); + +export const clearUserPasswordUpdateError = (id) => ({ + type: ActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR, + payload: { + id, + }, +}); + export const deleteUser = (id) => ({ type: ActionTypes.USER_DELETE, payload: { @@ -141,6 +155,53 @@ export const updateUserReceived = (user) => ({ }, }); +export const updateUserEmailRequested = (id, data) => ({ + type: ActionTypes.USER_EMAIL_UPDATE_REQUESTED, + payload: { + id, + data, + }, +}); + +export const updateUserEmailSucceeded = (id, email) => ({ + type: ActionTypes.USER_EMAIL_UPDATE_SUCCEEDED, + payload: { + id, + email, + }, +}); + +export const updateUserEmailFailed = (id, error) => ({ + type: ActionTypes.USER_EMAIL_UPDATE_FAILED, + payload: { + id, + error, + }, +}); + +export const updateUserPasswordRequested = (id, data) => ({ + type: ActionTypes.USER_PASSWORD_UPDATE_REQUESTED, + payload: { + id, + data, + }, +}); + +export const updateUserPasswordSucceeded = (id) => ({ + type: ActionTypes.USER_PASSWORD_UPDATE_SUCCEEDED, + payload: { + id, + }, +}); + +export const updateUserPasswordFailed = (id, error) => ({ + type: ActionTypes.USER_PASSWORD_UPDATE_FAILED, + payload: { + id, + error, + }, +}); + export const uploadUserAvatarRequested = (id) => ({ type: ActionTypes.USER_AVATAR_UPLOAD_REQUESTED, payload: { @@ -148,10 +209,11 @@ export const uploadUserAvatarRequested = (id) => ({ }, }); -export const uploadUserAvatarSucceeded = (user) => ({ +export const uploadUserAvatarSucceeded = (id, avatar) => ({ type: ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED, payload: { - user, + id, + avatar, }, }); diff --git a/client/src/api/users.js b/client/src/api/users.js index 98c19d2b..e4b65fe7 100755 --- a/client/src/api/users.js +++ b/client/src/api/users.js @@ -11,6 +11,10 @@ const getCurrentUser = (headers) => socket.get('/users/me', undefined, headers); const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers); +const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers); + +const updateUserPassword = (id, data, headers) => socket.patch(`/users/${id}/password`, data, headers); + const uploadUserAvatar = (id, file, headers) => http.post( `/users/${id}/upload-avatar`, { @@ -26,6 +30,8 @@ export default { createUser, getCurrentUser, updateUser, + updateUserEmail, + updateUserPassword, uploadUserAvatar, deleteUser, }; diff --git a/client/src/components/AddProjectModal/AddProjectModal.jsx b/client/src/components/AddProjectModal/AddProjectModal.jsx index 7e63cab2..5a0d2796 100755 --- a/client/src/components/AddProjectModal/AddProjectModal.jsx +++ b/client/src/components/AddProjectModal/AddProjectModal.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { @@ -6,7 +6,7 @@ import { } from 'semantic-ui-react'; import { Input } from '../../lib/custom-ui'; -import { useDeepCompareCallback, useForm } from '../../hooks'; +import { useForm } from '../../hooks'; import styles from './AddProjectModal.module.css'; @@ -22,7 +22,7 @@ const AddProjectModal = React.memo(({ const nameField = useRef(null); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, name: data.name.trim(), diff --git a/client/src/components/AddUserPopup/AddUserPopup.jsx b/client/src/components/AddUserPopup/AddUserPopup.jsx index 4b8c2d52..b1d91f7c 100755 --- a/client/src/components/AddUserPopup/AddUserPopup.jsx +++ b/client/src/components/AddUserPopup/AddUserPopup.jsx @@ -1,17 +1,35 @@ import isEmail from 'validator/lib/isEmail'; -import React, { useEffect, useRef } from 'react'; +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 { withPopup } from '../../lib/popup'; import { Input, Popup } from '../../lib/custom-ui'; -import { - useDeepCompareCallback, useDeepCompareEffect, useForm, usePrevious, -} from '../../hooks'; +import { useForm, usePrevious } from '../../hooks'; import styles from './AddUserPopup.module.css'; +const createMessage = (error) => { + if (!error) { + return error; + } + + if (error.message === 'User is already exist') { + return { + type: 'error', + content: 'common.userIsAlreadyExist', + }; + } + + return { + type: 'warning', + content: 'common.unknownError', + }; +}; + const AddUserPopup = React.memo( ({ defaultData, isSubmitting, error, onCreate, onMessageDismiss, onClose, @@ -26,11 +44,13 @@ const AddUserPopup = React.memo( ...defaultData, })); + const message = useMemo(() => createMessage(error), [error]); + const emailField = useRef(null); const passwordField = useRef(null); const nameField = useRef(null); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, email: data.email.trim(), @@ -59,11 +79,11 @@ const AddUserPopup = React.memo( emailField.current.select(); }, []); - useDeepCompareEffect(() => { + useEffect(() => { if (wasSubmitting && !isSubmitting) { if (!error) { onClose(); - } else if (error.message === 'userIsAlreadyExist') { + } else if (error.message === 'User is already exist') { emailField.current.select(); } } @@ -77,14 +97,14 @@ const AddUserPopup = React.memo( })} - {error && ( + {message && ( )} diff --git a/client/src/components/Board/AddList.jsx b/client/src/components/Board/AddList.jsx index 60bdcbfc..da06658a 100755 --- a/client/src/components/Board/AddList.jsx +++ b/client/src/components/Board/AddList.jsx @@ -6,11 +6,7 @@ import { useTranslation } from 'react-i18next'; import { Button, Form, Input } from 'semantic-ui-react'; import { - useClosableForm, - useDeepCompareCallback, - useDidUpdate, - useForm, - useToggle, + useClosableForm, useDidUpdate, useForm, useToggle, } from '../../hooks'; import styles from './AddList.module.css'; @@ -62,7 +58,7 @@ const AddList = React.forwardRef(({ children, onCreate }, ref) => { close, ); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, name: data.name.trim(), diff --git a/client/src/components/Boards/AddPopup.jsx b/client/src/components/Boards/AddPopup.jsx index 1147b371..dd0bb04b 100755 --- a/client/src/components/Boards/AddPopup.jsx +++ b/client/src/components/Boards/AddPopup.jsx @@ -1,11 +1,11 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { Button, Form } from 'semantic-ui-react'; import { withPopup } from '../../lib/popup'; import { Input, Popup } from '../../lib/custom-ui'; -import { useDeepCompareCallback, useForm } from '../../hooks'; +import { useForm } from '../../hooks'; import styles from './AddPopup.module.css'; @@ -18,7 +18,7 @@ const AddStep = React.memo(({ onCreate, onClose }) => { const nameField = useRef(null); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, name: data.name.trim(), diff --git a/client/src/components/Boards/EditPopup.jsx b/client/src/components/Boards/EditPopup.jsx index 008e8f40..f86ca3b2 100755 --- a/client/src/components/Boards/EditPopup.jsx +++ b/client/src/components/Boards/EditPopup.jsx @@ -6,7 +6,7 @@ import { Button, Form } from 'semantic-ui-react'; import { withPopup } from '../../lib/popup'; import { Input, Popup } from '../../lib/custom-ui'; -import { useDeepCompareCallback, useForm, useSteps } from '../../hooks'; +import { useForm, useSteps } from '../../hooks'; import DeleteStep from '../DeleteStep'; import styles from './EditPopup.module.css'; @@ -29,7 +29,7 @@ const EditStep = React.memo(({ const nameField = useRef(null); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, name: data.name.trim(), diff --git a/client/src/components/CardModal/Actions/AddComment.jsx b/client/src/components/CardModal/Actions/AddComment.jsx index 4e139fbf..b2e9d229 100755 --- a/client/src/components/CardModal/Actions/AddComment.jsx +++ b/client/src/components/CardModal/Actions/AddComment.jsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import TextareaAutosize from 'react-textarea-autosize'; import { Button, Form, TextArea } from 'semantic-ui-react'; -import { useDeepCompareCallback, useForm } from '../../../hooks'; +import { useForm } from '../../../hooks'; import styles from './AddComment.module.css'; @@ -18,7 +18,7 @@ const AddComment = React.memo(({ onCreate }) => { const textField = useRef(null); - const submit = useDeepCompareCallback(() => { + const submit = useCallback(() => { const cleanData = { ...data, text: data.text.trim(), diff --git a/client/src/components/CardModal/Actions/EditComment.jsx b/client/src/components/CardModal/Actions/EditComment.jsx index e85ad9ef..970ad16b 100755 --- a/client/src/components/CardModal/Actions/EditComment.jsx +++ b/client/src/components/CardModal/Actions/EditComment.jsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import TextareaAutosize from 'react-textarea-autosize'; import { Button, Form, TextArea } from 'semantic-ui-react'; -import { useClosableForm, useDeepCompareCallback, useForm } from '../../../hooks'; +import { useClosableForm, useForm } from '../../../hooks'; import styles from './EditComment.module.css'; @@ -18,7 +18,7 @@ const EditComment = React.forwardRef(({ children, defaultData, onUpdate }, ref) const textField = useRef(null); - const open = useDeepCompareCallback(() => { + const open = useCallback(() => { setIsOpened(true); setData({ text: '', @@ -31,7 +31,7 @@ const EditComment = React.forwardRef(({ children, defaultData, onUpdate }, ref) setData(null); }, [setData]); - const submit = useDeepCompareCallback(() => { + const submit = useCallback(() => { const cleanData = { ...data, text: data.text.trim(), diff --git a/client/src/components/CardModal/Tasks/Add.jsx b/client/src/components/CardModal/Tasks/Add.jsx index 524bc7a5..ed7df433 100755 --- a/client/src/components/CardModal/Tasks/Add.jsx +++ b/client/src/components/CardModal/Tasks/Add.jsx @@ -7,11 +7,7 @@ import TextareaAutosize from 'react-textarea-autosize'; import { Button, Form, TextArea } from 'semantic-ui-react'; import { - useClosableForm, - useDeepCompareCallback, - useDidUpdate, - useForm, - useToggle, + useClosableForm, useDidUpdate, useForm, useToggle, } from '../../../hooks'; import styles from './Add.module.css'; @@ -36,7 +32,7 @@ const Add = React.forwardRef(({ children, onCreate }, ref) => { setIsOpened(false); }, []); - const submit = useDeepCompareCallback(() => { + const submit = useCallback(() => { const cleanData = { ...data, name: data.name.trim(), diff --git a/client/src/components/EditDueDateStep/EditDueDateStep.jsx b/client/src/components/EditDueDateStep/EditDueDateStep.jsx index 3bf739ed..2cbb5e65 100755 --- a/client/src/components/EditDueDateStep/EditDueDateStep.jsx +++ b/client/src/components/EditDueDateStep/EditDueDateStep.jsx @@ -7,9 +7,7 @@ import DatePicker from 'react-datepicker'; import { Button, Form } from 'semantic-ui-react'; import { Input, Popup } from '../../lib/custom-ui'; -import { - useDeepCompareCallback, useDidUpdate, useForm, useToggle, -} from '../../hooks'; +import { useDidUpdate, useForm, useToggle } from '../../hooks'; import styles from './EditDueDateStep.module.css'; @@ -65,7 +63,7 @@ const EditDueDateStep = React.memo(({ [setData, selectTimeField, t], ); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { if (!nullableDate) { dateField.current.select(); return; @@ -86,9 +84,9 @@ const EditDueDateStep = React.memo(({ } onClose(); - }, [defaultValue, onUpdate, onClose, data, nullableDate]); + }, [defaultValue, onUpdate, onClose, data, nullableDate, t]); - const handleClearClick = useDeepCompareCallback(() => { + const handleClearClick = useCallback(() => { if (defaultValue) { onUpdate(null); } diff --git a/client/src/components/EditTimerStep/EditTimerStep.jsx b/client/src/components/EditTimerStep/EditTimerStep.jsx index 980e5f65..36c91d80 100644 --- a/client/src/components/EditTimerStep/EditTimerStep.jsx +++ b/client/src/components/EditTimerStep/EditTimerStep.jsx @@ -1,11 +1,11 @@ import dequal from 'dequal'; -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { Button, Form } from 'semantic-ui-react'; import { Input, Popup } from '../../lib/custom-ui'; -import { useDeepCompareCallback, useForm, useToggle } from '../../hooks'; +import { useForm, useToggle } from '../../hooks'; import { createTimer, getTimerParts, startTimer, stopTimer, updateTimer, } from '../../utils/timer'; @@ -41,16 +41,16 @@ const EditTimerStep = React.memo(({ const minutesField = useRef(null); const secondsField = useRef(null); - const handleStartClick = useDeepCompareCallback(() => { + const handleStartClick = useCallback(() => { onUpdate(startTimer(defaultValue)); onClose(); }, [defaultValue, onUpdate, onClose]); - const handleStopClick = useDeepCompareCallback(() => { + const handleStopClick = useCallback(() => { onUpdate(stopTimer(defaultValue)); }, [defaultValue, onUpdate]); - const handleClearClick = useDeepCompareCallback(() => { + const handleClearClick = useCallback(() => { if (defaultValue) { onUpdate(null); } @@ -58,12 +58,12 @@ const EditTimerStep = React.memo(({ onClose(); }, [defaultValue, onUpdate, onClose]); - const handleToggleEditClick = useDeepCompareCallback(() => { + const handleToggleEditClick = useCallback(() => { setData(createData(defaultValue)); toggleEdit(); }, [defaultValue, setData, toggleEdit]); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const parts = { hours: parseInt(data.hours, 10), minutes: parseInt(data.minutes, 10), diff --git a/client/src/components/Header/Header.jsx b/client/src/components/Header/Header.jsx index 6a1cfa10..41c05311 100755 --- a/client/src/components/Header/Header.jsx +++ b/client/src/components/Header/Header.jsx @@ -5,7 +5,7 @@ import { Icon, Menu } from 'semantic-ui-react'; import Paths from '../../constants/Paths'; import NotificationsPopup from './NotificationsPopup'; -import UserPopup from './UserPopup'; +import UserPopup from '../UserPopup'; import styles from './Header.module.css'; @@ -14,10 +14,14 @@ const Header = React.memo( user, notifications, isEditable, + onUsers, + onNotificationDelete, onUserUpdate, onUserAvatarUpload, - onNotificationDelete, - onUsers, + onUserEmailUpdate, + onUserEmailUpdateMessageDismiss, + onUserPasswordUpdate, + onUserPasswordUpdateMessageDismiss, onLogout, }) => (
@@ -40,11 +44,18 @@ const Header = React.memo( {user.name} @@ -61,10 +72,14 @@ Header.propTypes = { notifications: PropTypes.array.isRequired, /* eslint-enable react/forbid-prop-types */ isEditable: PropTypes.bool.isRequired, + onUsers: PropTypes.func.isRequired, + onNotificationDelete: PropTypes.func.isRequired, onUserUpdate: PropTypes.func.isRequired, onUserAvatarUpload: PropTypes.func.isRequired, - onNotificationDelete: PropTypes.func.isRequired, - onUsers: PropTypes.func.isRequired, + onUserEmailUpdate: PropTypes.func.isRequired, + onUserEmailUpdateMessageDismiss: PropTypes.func.isRequired, + onUserPasswordUpdate: PropTypes.func.isRequired, + onUserPasswordUpdateMessageDismiss: PropTypes.func.isRequired, onLogout: PropTypes.func.isRequired, }; diff --git a/client/src/components/LabelsStep/AddStep.jsx b/client/src/components/LabelsStep/AddStep.jsx index c98ba7e0..42def293 100755 --- a/client/src/components/LabelsStep/AddStep.jsx +++ b/client/src/components/LabelsStep/AddStep.jsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { Button, Form } from 'semantic-ui-react'; import { Popup } from '../../lib/custom-ui'; -import { useDeepCompareCallback, useForm } from '../../hooks'; +import { useForm } from '../../hooks'; import LabelColors from '../../constants/LabelColors'; import Editor from './Editor'; @@ -18,7 +18,7 @@ const AddStep = React.memo(({ onCreate, onBack }) => { color: LabelColors.KEYS[0], })); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, name: data.name.trim() || null, diff --git a/client/src/components/LabelsStep/EditStep.jsx b/client/src/components/LabelsStep/EditStep.jsx index b288672f..8594b458 100755 --- a/client/src/components/LabelsStep/EditStep.jsx +++ b/client/src/components/LabelsStep/EditStep.jsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { Button, Form } from 'semantic-ui-react'; import { Popup } from '../../lib/custom-ui'; -import { useDeepCompareCallback, useForm, useSteps } from '../../hooks'; +import { useForm, useSteps } from '../../hooks'; import LabelColors from '../../constants/LabelColors'; import Editor from './Editor'; import DeleteStep from '../DeleteStep'; @@ -29,7 +29,7 @@ const EditStep = React.memo(({ const [step, openStep, handleBack] = useSteps(); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, name: data.name.trim() || null, diff --git a/client/src/components/List/AddCard.jsx b/client/src/components/List/AddCard.jsx index 975a1933..b3fe4a4c 100755 --- a/client/src/components/List/AddCard.jsx +++ b/client/src/components/List/AddCard.jsx @@ -7,11 +7,7 @@ import TextareaAutosize from 'react-textarea-autosize'; import { Button, Form, TextArea } from 'semantic-ui-react'; import { - useClosableForm, - useDeepCompareCallback, - useDidUpdate, - useForm, - useToggle, + useClosableForm, useDidUpdate, useForm, useToggle, } from '../../hooks'; import styles from './AddCard.module.css'; @@ -36,7 +32,7 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => { setIsOpened(false); }, []); - const submit = useDeepCompareCallback(() => { + const submit = useCallback(() => { const cleanData = { ...data, name: data.name.trim(), diff --git a/client/src/components/Login/Login.jsx b/client/src/components/Login/Login.jsx index 4838d2b4..e914c671 100755 --- a/client/src/components/Login/Login.jsx +++ b/client/src/components/Login/Login.jsx @@ -1,4 +1,6 @@ -import React, { useEffect, useRef } from 'react'; +import React, { + useCallback, useEffect, useMemo, useRef, +} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; @@ -9,16 +11,45 @@ import { import { Input } from '../../lib/custom-ui'; import { - useDeepCompareCallback, - useDeepCompareEffect, - useDidUpdate, - useForm, - usePrevious, - useToggle, + useDidUpdate, useForm, usePrevious, useToggle, } from '../../hooks'; import styles from './Login.module.css'; +const createMessage = (error) => { + if (!error) { + return error; + } + + switch (error.message) { + case 'Email does not exist': + return { + type: 'error', + content: 'common.emailDoesNotExist', + }; + case 'Password is not valid': + return { + type: 'error', + content: 'common.invalidPassword', + }; + case 'Failed to fetch': + return { + type: 'warning', + content: 'common.noInternetConnection', + }; + case 'Network request failed': + return { + type: 'warning', + content: 'common.serverConnectionFailed', + }; + default: + return { + type: 'warning', + content: 'common.unknownError', + }; + } +}; + const Login = React.memo( ({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss, @@ -32,12 +63,13 @@ const Login = React.memo( ...defaultData, })); + const message = useMemo(() => createMessage(error), [error]); const [focusPasswordFieldState, focusPasswordField] = useToggle(); const emailField = useRef(null); const passwordField = useRef(null); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, email: data.email.trim(), @@ -60,14 +92,14 @@ const Login = React.memo( emailField.current.select(); }, []); - useDeepCompareEffect(() => { + useEffect(() => { if (wasSubmitting && !isSubmitting && error) { switch (error.message) { - case 'emailDoesNotExist': + case 'Email does not exist': emailField.current.select(); break; - case 'invalidPassword': + case 'Password is not valid': setData((prevData) => ({ ...prevData, password: '', @@ -98,14 +130,14 @@ const Login = React.memo( className={styles.formTitle} />
- {error && ( + {message && ( )} diff --git a/client/src/components/Project/EditPopup.jsx b/client/src/components/Project/EditPopup.jsx index 229e3b84..51b52aa7 100755 --- a/client/src/components/Project/EditPopup.jsx +++ b/client/src/components/Project/EditPopup.jsx @@ -6,7 +6,7 @@ import { Button, Form } from 'semantic-ui-react'; import { withPopup } from '../../lib/popup'; import { Input, Popup } from '../../lib/custom-ui'; -import { useDeepCompareCallback, useForm, useSteps } from '../../hooks'; +import { useForm, useSteps } from '../../hooks'; import DeleteStep from '../DeleteStep'; import styles from './EditPopup.module.css'; @@ -29,7 +29,7 @@ const EditStep = React.memo(({ const nameField = useRef(null); - const handleSubmit = useDeepCompareCallback(() => { + const handleSubmit = useCallback(() => { const cleanData = { ...data, name: data.name.trim(), diff --git a/client/src/components/Timer/Timer.jsx b/client/src/components/Timer/Timer.jsx index c97d5d96..c9a944ce 100644 --- a/client/src/components/Timer/Timer.jsx +++ b/client/src/components/Timer/Timer.jsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { useDeepCompareEffect, useForceUpdate, usePrevious } from '../../hooks'; +import { useForceUpdate, usePrevious } from '../../hooks'; import { formatTimer } from '../../utils/timer'; import styles from './Timer.module.css'; @@ -50,7 +50,7 @@ const Timer = React.memo(({ clearInterval(interval.current); }, []); - useDeepCompareEffect(() => { + useEffect(() => { if (prevStartedAt) { if (!startedAt) { stop(); diff --git a/client/src/components/Header/UserPopup/EditAvatarStep.jsx b/client/src/components/UserPopup/EditAvatarStep.jsx similarity index 95% rename from client/src/components/Header/UserPopup/EditAvatarStep.jsx rename to client/src/components/UserPopup/EditAvatarStep.jsx index 452747ad..5141728e 100755 --- a/client/src/components/Header/UserPopup/EditAvatarStep.jsx +++ b/client/src/components/UserPopup/EditAvatarStep.jsx @@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { Button } from 'semantic-ui-react'; -import { Popup } from '../../../lib/custom-ui'; +import { Popup } from '../../lib/custom-ui'; -import User from '../../User'; +import User from '../User'; import styles from './EditAvatarStep.module.css'; diff --git a/client/src/components/Header/UserPopup/EditAvatarStep.module.css b/client/src/components/UserPopup/EditAvatarStep.module.css similarity index 100% rename from client/src/components/Header/UserPopup/EditAvatarStep.module.css rename to client/src/components/UserPopup/EditAvatarStep.module.css diff --git a/client/src/components/UserPopup/EditEmailStep.jsx b/client/src/components/UserPopup/EditEmailStep.jsx new file mode 100644 index 00000000..f0af8028 --- /dev/null +++ b/client/src/components/UserPopup/EditEmailStep.jsx @@ -0,0 +1,185 @@ +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 { Input, Popup } from '../../lib/custom-ui'; + +import { + useDidUpdate, useForm, usePrevious, useToggle, +} from '../../hooks'; + +import styles from './EditNameStep.module.css'; + +const createMessage = (error) => { + if (!error) { + return error; + } + + switch (error.message) { + case 'User is already exist': + return { + type: 'error', + content: 'common.userIsAlreadyExist', + }; + case 'Current password is not valid': + return { + type: 'error', + content: 'common.invalidCurrentPassword', + }; + default: + return { + type: 'warning', + content: 'common.unknownError', + }; + } +}; + +const EditEmailStep = React.memo(({ + defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onBack, 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 'User is already exist': + emailField.current.select(); + + break; + case 'Current password is not valid': + setData((prevData) => ({ + ...prevData, + currentPassword: '', + })); + focusCurrentPasswordField(); + + break; + default: + } + } else { + onClose(); + } + } + }, [isSubmitting, wasSubmitting, error, onClose, setData, focusCurrentPasswordField]); + + useDidUpdate(() => { + currentPasswordField.current.focus(); + }, [focusCurrentPasswordFieldState]); + + return ( + <> + + {t('common.editEmail', { + context: 'title', + })} + + + {message && ( + + )} +
+
{t('common.newEmail')}
+ + {data.email.trim() !== email && ( + <> +
{t('common.currentPassword')}
+ + + )} +