mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
Add email and password change functionality for a current user, remove deep compare hooks
This commit is contained in:
parent
b53e5bf94c
commit
680d664279
67 changed files with 1232 additions and 267 deletions
|
@ -7,8 +7,8 @@ export const authenticate = (data) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clearAuthenticationError = () => ({
|
export const clearAuthenticateError = () => ({
|
||||||
type: EntryActionTypes.AUTHENTICATION_ERROR_CLEAR,
|
type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ export const createUser = (data) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clearUserCreationError = () => ({
|
export const clearUserCreateError = () => ({
|
||||||
type: EntryActionTypes.USER_CREATION_ERROR_CLEAR,
|
type: EntryActionTypes.USER_CREATE_ERROR_CLEAR,
|
||||||
payload: {},
|
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) => ({
|
export const uploadCurrentUserAvatar = (file) => ({
|
||||||
type: EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD,
|
type: EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD,
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
@ -9,8 +9,8 @@ export const authenticate = (data) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clearAuthenticationError = () => ({
|
export const clearAuthenticateError = () => ({
|
||||||
type: ActionTypes.AUTHENTICATION_ERROR_CLEAR,
|
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ export const createUser = (data) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clearUserCreationError = () => ({
|
export const clearUserCreateError = () => ({
|
||||||
type: ActionTypes.USER_CREATION_ERROR_CLEAR,
|
type: ActionTypes.USER_CREATE_ERROR_CLEAR,
|
||||||
payload: {},
|
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) => ({
|
export const deleteUser = (id) => ({
|
||||||
type: ActionTypes.USER_DELETE,
|
type: ActionTypes.USER_DELETE,
|
||||||
payload: {
|
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) => ({
|
export const uploadUserAvatarRequested = (id) => ({
|
||||||
type: ActionTypes.USER_AVATAR_UPLOAD_REQUESTED,
|
type: ActionTypes.USER_AVATAR_UPLOAD_REQUESTED,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -148,10 +209,11 @@ export const uploadUserAvatarRequested = (id) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uploadUserAvatarSucceeded = (user) => ({
|
export const uploadUserAvatarSucceeded = (id, avatar) => ({
|
||||||
type: ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED,
|
type: ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED,
|
||||||
payload: {
|
payload: {
|
||||||
user,
|
id,
|
||||||
|
avatar,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 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(
|
const uploadUserAvatar = (id, file, headers) => http.post(
|
||||||
`/users/${id}/upload-avatar`,
|
`/users/${id}/upload-avatar`,
|
||||||
{
|
{
|
||||||
|
@ -26,6 +30,8 @@ export default {
|
||||||
createUser,
|
createUser,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
|
updateUserEmail,
|
||||||
|
updateUserPassword,
|
||||||
uploadUserAvatar,
|
uploadUserAvatar,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
|
@ -6,7 +6,7 @@ import {
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { Input } from '../../lib/custom-ui';
|
import { Input } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm } from '../../hooks';
|
import { useForm } from '../../hooks';
|
||||||
|
|
||||||
import styles from './AddProjectModal.module.css';
|
import styles from './AddProjectModal.module.css';
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ const AddProjectModal = React.memo(({
|
||||||
|
|
||||||
const nameField = useRef(null);
|
const nameField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
|
|
|
@ -1,17 +1,35 @@
|
||||||
import isEmail from 'validator/lib/isEmail';
|
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 PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Message } from 'semantic-ui-react';
|
import { Button, Form, Message } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../lib/popup';
|
import { withPopup } from '../../lib/popup';
|
||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import {
|
import { useForm, usePrevious } from '../../hooks';
|
||||||
useDeepCompareCallback, useDeepCompareEffect, useForm, usePrevious,
|
|
||||||
} from '../../hooks';
|
|
||||||
|
|
||||||
import styles from './AddUserPopup.module.css';
|
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(
|
const AddUserPopup = React.memo(
|
||||||
({
|
({
|
||||||
defaultData, isSubmitting, error, onCreate, onMessageDismiss, onClose,
|
defaultData, isSubmitting, error, onCreate, onMessageDismiss, onClose,
|
||||||
|
@ -26,11 +44,13 @@ const AddUserPopup = React.memo(
|
||||||
...defaultData,
|
...defaultData,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const message = useMemo(() => createMessage(error), [error]);
|
||||||
|
|
||||||
const emailField = useRef(null);
|
const emailField = useRef(null);
|
||||||
const passwordField = useRef(null);
|
const passwordField = useRef(null);
|
||||||
const nameField = useRef(null);
|
const nameField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
email: data.email.trim(),
|
email: data.email.trim(),
|
||||||
|
@ -59,11 +79,11 @@ const AddUserPopup = React.memo(
|
||||||
emailField.current.select();
|
emailField.current.select();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useDeepCompareEffect(() => {
|
useEffect(() => {
|
||||||
if (wasSubmitting && !isSubmitting) {
|
if (wasSubmitting && !isSubmitting) {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
onClose();
|
onClose();
|
||||||
} else if (error.message === 'userIsAlreadyExist') {
|
} else if (error.message === 'User is already exist') {
|
||||||
emailField.current.select();
|
emailField.current.select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,14 +97,14 @@ const AddUserPopup = React.memo(
|
||||||
})}
|
})}
|
||||||
</Popup.Header>
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
{error && (
|
{message && (
|
||||||
<Message
|
<Message
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...{
|
{...{
|
||||||
[error.type || 'error']: true,
|
[message.type]: true,
|
||||||
}}
|
}}
|
||||||
visible
|
visible
|
||||||
content={t(`common.${error.message}`)}
|
content={t(message.content)}
|
||||||
onDismiss={onMessageDismiss}
|
onDismiss={onMessageDismiss}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -6,11 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input } from 'semantic-ui-react';
|
import { Button, Form, Input } from 'semantic-ui-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useClosableForm,
|
useClosableForm, useDidUpdate, useForm, useToggle,
|
||||||
useDeepCompareCallback,
|
|
||||||
useDidUpdate,
|
|
||||||
useForm,
|
|
||||||
useToggle,
|
|
||||||
} from '../../hooks';
|
} from '../../hooks';
|
||||||
|
|
||||||
import styles from './AddList.module.css';
|
import styles from './AddList.module.css';
|
||||||
|
@ -62,7 +58,7 @@ const AddList = React.forwardRef(({ children, onCreate }, ref) => {
|
||||||
close,
|
close,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form } from 'semantic-ui-react';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../lib/popup';
|
import { withPopup } from '../../lib/popup';
|
||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm } from '../../hooks';
|
import { useForm } from '../../hooks';
|
||||||
|
|
||||||
import styles from './AddPopup.module.css';
|
import styles from './AddPopup.module.css';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ const AddStep = React.memo(({ onCreate, onClose }) => {
|
||||||
|
|
||||||
const nameField = useRef(null);
|
const nameField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Button, Form } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../lib/popup';
|
import { withPopup } from '../../lib/popup';
|
||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm, useSteps } from '../../hooks';
|
import { useForm, useSteps } from '../../hooks';
|
||||||
import DeleteStep from '../DeleteStep';
|
import DeleteStep from '../DeleteStep';
|
||||||
|
|
||||||
import styles from './EditPopup.module.css';
|
import styles from './EditPopup.module.css';
|
||||||
|
@ -29,7 +29,7 @@ const EditStep = React.memo(({
|
||||||
|
|
||||||
const nameField = useRef(null);
|
const nameField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
import { Button, Form, TextArea } from 'semantic-ui-react';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm } from '../../../hooks';
|
import { useForm } from '../../../hooks';
|
||||||
|
|
||||||
import styles from './AddComment.module.css';
|
import styles from './AddComment.module.css';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ const AddComment = React.memo(({ onCreate }) => {
|
||||||
|
|
||||||
const textField = useRef(null);
|
const textField = useRef(null);
|
||||||
|
|
||||||
const submit = useDeepCompareCallback(() => {
|
const submit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
text: data.text.trim(),
|
text: data.text.trim(),
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
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';
|
import styles from './EditComment.module.css';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ const EditComment = React.forwardRef(({ children, defaultData, onUpdate }, ref)
|
||||||
|
|
||||||
const textField = useRef(null);
|
const textField = useRef(null);
|
||||||
|
|
||||||
const open = useDeepCompareCallback(() => {
|
const open = useCallback(() => {
|
||||||
setIsOpened(true);
|
setIsOpened(true);
|
||||||
setData({
|
setData({
|
||||||
text: '',
|
text: '',
|
||||||
|
@ -31,7 +31,7 @@ const EditComment = React.forwardRef(({ children, defaultData, onUpdate }, ref)
|
||||||
setData(null);
|
setData(null);
|
||||||
}, [setData]);
|
}, [setData]);
|
||||||
|
|
||||||
const submit = useDeepCompareCallback(() => {
|
const submit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
text: data.text.trim(),
|
text: data.text.trim(),
|
||||||
|
|
|
@ -7,11 +7,7 @@ import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
import { Button, Form, TextArea } from 'semantic-ui-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useClosableForm,
|
useClosableForm, useDidUpdate, useForm, useToggle,
|
||||||
useDeepCompareCallback,
|
|
||||||
useDidUpdate,
|
|
||||||
useForm,
|
|
||||||
useToggle,
|
|
||||||
} from '../../../hooks';
|
} from '../../../hooks';
|
||||||
|
|
||||||
import styles from './Add.module.css';
|
import styles from './Add.module.css';
|
||||||
|
@ -36,7 +32,7 @@ const Add = React.forwardRef(({ children, onCreate }, ref) => {
|
||||||
setIsOpened(false);
|
setIsOpened(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submit = useDeepCompareCallback(() => {
|
const submit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
|
|
|
@ -7,9 +7,7 @@ import DatePicker from 'react-datepicker';
|
||||||
import { Button, Form } from 'semantic-ui-react';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import {
|
import { useDidUpdate, useForm, useToggle } from '../../hooks';
|
||||||
useDeepCompareCallback, useDidUpdate, useForm, useToggle,
|
|
||||||
} from '../../hooks';
|
|
||||||
|
|
||||||
import styles from './EditDueDateStep.module.css';
|
import styles from './EditDueDateStep.module.css';
|
||||||
|
|
||||||
|
@ -65,7 +63,7 @@ const EditDueDateStep = React.memo(({
|
||||||
[setData, selectTimeField, t],
|
[setData, selectTimeField, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
if (!nullableDate) {
|
if (!nullableDate) {
|
||||||
dateField.current.select();
|
dateField.current.select();
|
||||||
return;
|
return;
|
||||||
|
@ -86,9 +84,9 @@ const EditDueDateStep = React.memo(({
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [defaultValue, onUpdate, onClose, data, nullableDate]);
|
}, [defaultValue, onUpdate, onClose, data, nullableDate, t]);
|
||||||
|
|
||||||
const handleClearClick = useDeepCompareCallback(() => {
|
const handleClearClick = useCallback(() => {
|
||||||
if (defaultValue) {
|
if (defaultValue) {
|
||||||
onUpdate(null);
|
onUpdate(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import dequal from 'dequal';
|
import dequal from 'dequal';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form } from 'semantic-ui-react';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm, useToggle } from '../../hooks';
|
import { useForm, useToggle } from '../../hooks';
|
||||||
import {
|
import {
|
||||||
createTimer, getTimerParts, startTimer, stopTimer, updateTimer,
|
createTimer, getTimerParts, startTimer, stopTimer, updateTimer,
|
||||||
} from '../../utils/timer';
|
} from '../../utils/timer';
|
||||||
|
@ -41,16 +41,16 @@ const EditTimerStep = React.memo(({
|
||||||
const minutesField = useRef(null);
|
const minutesField = useRef(null);
|
||||||
const secondsField = useRef(null);
|
const secondsField = useRef(null);
|
||||||
|
|
||||||
const handleStartClick = useDeepCompareCallback(() => {
|
const handleStartClick = useCallback(() => {
|
||||||
onUpdate(startTimer(defaultValue));
|
onUpdate(startTimer(defaultValue));
|
||||||
onClose();
|
onClose();
|
||||||
}, [defaultValue, onUpdate, onClose]);
|
}, [defaultValue, onUpdate, onClose]);
|
||||||
|
|
||||||
const handleStopClick = useDeepCompareCallback(() => {
|
const handleStopClick = useCallback(() => {
|
||||||
onUpdate(stopTimer(defaultValue));
|
onUpdate(stopTimer(defaultValue));
|
||||||
}, [defaultValue, onUpdate]);
|
}, [defaultValue, onUpdate]);
|
||||||
|
|
||||||
const handleClearClick = useDeepCompareCallback(() => {
|
const handleClearClick = useCallback(() => {
|
||||||
if (defaultValue) {
|
if (defaultValue) {
|
||||||
onUpdate(null);
|
onUpdate(null);
|
||||||
}
|
}
|
||||||
|
@ -58,12 +58,12 @@ const EditTimerStep = React.memo(({
|
||||||
onClose();
|
onClose();
|
||||||
}, [defaultValue, onUpdate, onClose]);
|
}, [defaultValue, onUpdate, onClose]);
|
||||||
|
|
||||||
const handleToggleEditClick = useDeepCompareCallback(() => {
|
const handleToggleEditClick = useCallback(() => {
|
||||||
setData(createData(defaultValue));
|
setData(createData(defaultValue));
|
||||||
toggleEdit();
|
toggleEdit();
|
||||||
}, [defaultValue, setData, toggleEdit]);
|
}, [defaultValue, setData, toggleEdit]);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const parts = {
|
const parts = {
|
||||||
hours: parseInt(data.hours, 10),
|
hours: parseInt(data.hours, 10),
|
||||||
minutes: parseInt(data.minutes, 10),
|
minutes: parseInt(data.minutes, 10),
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Icon, Menu } from 'semantic-ui-react';
|
||||||
|
|
||||||
import Paths from '../../constants/Paths';
|
import Paths from '../../constants/Paths';
|
||||||
import NotificationsPopup from './NotificationsPopup';
|
import NotificationsPopup from './NotificationsPopup';
|
||||||
import UserPopup from './UserPopup';
|
import UserPopup from '../UserPopup';
|
||||||
|
|
||||||
import styles from './Header.module.css';
|
import styles from './Header.module.css';
|
||||||
|
|
||||||
|
@ -14,10 +14,14 @@ const Header = React.memo(
|
||||||
user,
|
user,
|
||||||
notifications,
|
notifications,
|
||||||
isEditable,
|
isEditable,
|
||||||
|
onUsers,
|
||||||
|
onNotificationDelete,
|
||||||
onUserUpdate,
|
onUserUpdate,
|
||||||
onUserAvatarUpload,
|
onUserAvatarUpload,
|
||||||
onNotificationDelete,
|
onUserEmailUpdate,
|
||||||
onUsers,
|
onUserEmailUpdateMessageDismiss,
|
||||||
|
onUserPasswordUpdate,
|
||||||
|
onUserPasswordUpdateMessageDismiss,
|
||||||
onLogout,
|
onLogout,
|
||||||
}) => (
|
}) => (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
|
@ -40,11 +44,18 @@ const Header = React.memo(
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</NotificationsPopup>
|
</NotificationsPopup>
|
||||||
<UserPopup
|
<UserPopup
|
||||||
|
email={user.email}
|
||||||
name={user.name}
|
name={user.name}
|
||||||
avatar={user.avatar}
|
avatar={user.avatar}
|
||||||
isAvatarUploading={user.isAvatarUploading}
|
isAvatarUploading={user.isAvatarUploading}
|
||||||
|
emailUpdateForm={user.emailUpdateForm}
|
||||||
|
passwordUpdateForm={user.passwordUpdateForm}
|
||||||
onUpdate={onUserUpdate}
|
onUpdate={onUserUpdate}
|
||||||
onAvatarUpload={onUserAvatarUpload}
|
onAvatarUpload={onUserAvatarUpload}
|
||||||
|
onEmailUpdate={onUserEmailUpdate}
|
||||||
|
onEmailUpdateMessageDismiss={onUserEmailUpdateMessageDismiss}
|
||||||
|
onPasswordUpdate={onUserPasswordUpdate}
|
||||||
|
onPasswordUpdateMessageDismiss={onUserPasswordUpdateMessageDismiss}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
>
|
>
|
||||||
<Menu.Item className={styles.item}>{user.name}</Menu.Item>
|
<Menu.Item className={styles.item}>{user.name}</Menu.Item>
|
||||||
|
@ -61,10 +72,14 @@ Header.propTypes = {
|
||||||
notifications: PropTypes.array.isRequired,
|
notifications: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
isEditable: PropTypes.bool.isRequired,
|
isEditable: PropTypes.bool.isRequired,
|
||||||
|
onUsers: PropTypes.func.isRequired,
|
||||||
|
onNotificationDelete: PropTypes.func.isRequired,
|
||||||
onUserUpdate: PropTypes.func.isRequired,
|
onUserUpdate: PropTypes.func.isRequired,
|
||||||
onUserAvatarUpload: PropTypes.func.isRequired,
|
onUserAvatarUpload: PropTypes.func.isRequired,
|
||||||
onNotificationDelete: PropTypes.func.isRequired,
|
onUserEmailUpdate: PropTypes.func.isRequired,
|
||||||
onUsers: PropTypes.func.isRequired,
|
onUserEmailUpdateMessageDismiss: PropTypes.func.isRequired,
|
||||||
|
onUserPasswordUpdate: PropTypes.func.isRequired,
|
||||||
|
onUserPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form } from 'semantic-ui-react';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import { Popup } from '../../lib/custom-ui';
|
import { Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm } from '../../hooks';
|
import { useForm } from '../../hooks';
|
||||||
import LabelColors from '../../constants/LabelColors';
|
import LabelColors from '../../constants/LabelColors';
|
||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ const AddStep = React.memo(({ onCreate, onBack }) => {
|
||||||
color: LabelColors.KEYS[0],
|
color: LabelColors.KEYS[0],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim() || null,
|
name: data.name.trim() || null,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form } from 'semantic-ui-react';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import { Popup } from '../../lib/custom-ui';
|
import { Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm, useSteps } from '../../hooks';
|
import { useForm, useSteps } from '../../hooks';
|
||||||
import LabelColors from '../../constants/LabelColors';
|
import LabelColors from '../../constants/LabelColors';
|
||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
import DeleteStep from '../DeleteStep';
|
import DeleteStep from '../DeleteStep';
|
||||||
|
@ -29,7 +29,7 @@ const EditStep = React.memo(({
|
||||||
|
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim() || null,
|
name: data.name.trim() || null,
|
||||||
|
|
|
@ -7,11 +7,7 @@ import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { Button, Form, TextArea } from 'semantic-ui-react';
|
import { Button, Form, TextArea } from 'semantic-ui-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useClosableForm,
|
useClosableForm, useDidUpdate, useForm, useToggle,
|
||||||
useDeepCompareCallback,
|
|
||||||
useDidUpdate,
|
|
||||||
useForm,
|
|
||||||
useToggle,
|
|
||||||
} from '../../hooks';
|
} from '../../hooks';
|
||||||
|
|
||||||
import styles from './AddCard.module.css';
|
import styles from './AddCard.module.css';
|
||||||
|
@ -36,7 +32,7 @@ const AddCard = React.forwardRef(({ children, onCreate }, ref) => {
|
||||||
setIsOpened(false);
|
setIsOpened(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submit = useDeepCompareCallback(() => {
|
const submit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, {
|
||||||
|
useCallback, useEffect, useMemo, useRef,
|
||||||
|
} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -9,16 +11,45 @@ import {
|
||||||
import { Input } from '../../lib/custom-ui';
|
import { Input } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useDeepCompareCallback,
|
useDidUpdate, useForm, usePrevious, useToggle,
|
||||||
useDeepCompareEffect,
|
|
||||||
useDidUpdate,
|
|
||||||
useForm,
|
|
||||||
usePrevious,
|
|
||||||
useToggle,
|
|
||||||
} from '../../hooks';
|
} from '../../hooks';
|
||||||
|
|
||||||
import styles from './Login.module.css';
|
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(
|
const Login = React.memo(
|
||||||
({
|
({
|
||||||
defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss,
|
defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss,
|
||||||
|
@ -32,12 +63,13 @@ const Login = React.memo(
|
||||||
...defaultData,
|
...defaultData,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const message = useMemo(() => createMessage(error), [error]);
|
||||||
const [focusPasswordFieldState, focusPasswordField] = useToggle();
|
const [focusPasswordFieldState, focusPasswordField] = useToggle();
|
||||||
|
|
||||||
const emailField = useRef(null);
|
const emailField = useRef(null);
|
||||||
const passwordField = useRef(null);
|
const passwordField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
email: data.email.trim(),
|
email: data.email.trim(),
|
||||||
|
@ -60,14 +92,14 @@ const Login = React.memo(
|
||||||
emailField.current.select();
|
emailField.current.select();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useDeepCompareEffect(() => {
|
useEffect(() => {
|
||||||
if (wasSubmitting && !isSubmitting && error) {
|
if (wasSubmitting && !isSubmitting && error) {
|
||||||
switch (error.message) {
|
switch (error.message) {
|
||||||
case 'emailDoesNotExist':
|
case 'Email does not exist':
|
||||||
emailField.current.select();
|
emailField.current.select();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'invalidPassword':
|
case 'Password is not valid':
|
||||||
setData((prevData) => ({
|
setData((prevData) => ({
|
||||||
...prevData,
|
...prevData,
|
||||||
password: '',
|
password: '',
|
||||||
|
@ -98,14 +130,14 @@ const Login = React.memo(
|
||||||
className={styles.formTitle}
|
className={styles.formTitle}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{error && (
|
{message && (
|
||||||
<Message
|
<Message
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...{
|
{...{
|
||||||
[error.type || 'error']: true,
|
[message.type]: true,
|
||||||
}}
|
}}
|
||||||
visible
|
visible
|
||||||
content={t(`common.${error.message}`)}
|
content={t(message.content)}
|
||||||
onDismiss={onMessageDismiss}
|
onDismiss={onMessageDismiss}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Button, Form } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../lib/popup';
|
import { withPopup } from '../../lib/popup';
|
||||||
import { Input, Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useDeepCompareCallback, useForm, useSteps } from '../../hooks';
|
import { useForm, useSteps } from '../../hooks';
|
||||||
import DeleteStep from '../DeleteStep';
|
import DeleteStep from '../DeleteStep';
|
||||||
|
|
||||||
import styles from './EditPopup.module.css';
|
import styles from './EditPopup.module.css';
|
||||||
|
@ -29,7 +29,7 @@ const EditStep = React.memo(({
|
||||||
|
|
||||||
const nameField = useRef(null);
|
const nameField = useRef(null);
|
||||||
|
|
||||||
const handleSubmit = useDeepCompareCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const cleanData = {
|
const cleanData = {
|
||||||
...data,
|
...data,
|
||||||
name: data.name.trim(),
|
name: data.name.trim(),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useDeepCompareEffect, useForceUpdate, usePrevious } from '../../hooks';
|
import { useForceUpdate, usePrevious } from '../../hooks';
|
||||||
import { formatTimer } from '../../utils/timer';
|
import { formatTimer } from '../../utils/timer';
|
||||||
|
|
||||||
import styles from './Timer.module.css';
|
import styles from './Timer.module.css';
|
||||||
|
@ -50,7 +50,7 @@ const Timer = React.memo(({
|
||||||
clearInterval(interval.current);
|
clearInterval(interval.current);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useDeepCompareEffect(() => {
|
useEffect(() => {
|
||||||
if (prevStartedAt) {
|
if (prevStartedAt) {
|
||||||
if (!startedAt) {
|
if (!startedAt) {
|
||||||
stop();
|
stop();
|
||||||
|
|
|
@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from 'semantic-ui-react';
|
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';
|
import styles from './EditAvatarStep.module.css';
|
||||||
|
|
185
client/src/components/UserPopup/EditEmailStep.jsx
Normal file
185
client/src/components/UserPopup/EditEmailStep.jsx
Normal file
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Popup.Header onBack={onBack}>
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
{data.email.trim() !== email && (
|
||||||
|
<>
|
||||||
|
<div className={styles.text}>{t('common.currentPassword')}</div>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
ref={currentPasswordField}
|
||||||
|
type="password"
|
||||||
|
name="currentPassword"
|
||||||
|
value={data.currentPassword}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
positive
|
||||||
|
content={t('action.save')}
|
||||||
|
loading={isSubmitting}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Popup.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
EditEmailStep.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,
|
||||||
|
onBack: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditEmailStep.defaultProps = {
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditEmailStep;
|
|
@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form } from 'semantic-ui-react';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import { Input, Popup } from '../../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useField } from '../../../hooks';
|
import { useField } from '../../hooks';
|
||||||
|
|
||||||
import styles from './EditNameStep.module.css';
|
import styles from './EditNameStep.module.css';
|
||||||
|
|
10
client/src/components/UserPopup/EditNameStep.module.css
Normal file
10
client/src/components/UserPopup/EditNameStep.module.css
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: #444444;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
153
client/src/components/UserPopup/EditPasswordStep.jsx
Normal file
153
client/src/components/UserPopup/EditPasswordStep.jsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
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 'Current password is not valid':
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
content: 'common.invalidCurrentPassword',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
type: 'warning',
|
||||||
|
content: 'common.unknownError',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditPasswordStep = React.memo(({
|
||||||
|
defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onBack, 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 === 'Current password is not valid') {
|
||||||
|
setData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
currentPassword: '',
|
||||||
|
}));
|
||||||
|
focusCurrentPasswordField();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isSubmitting, wasSubmitting, error, onClose, setData, focusCurrentPasswordField]);
|
||||||
|
|
||||||
|
useDidUpdate(() => {
|
||||||
|
currentPasswordField.current.focus();
|
||||||
|
}, [focusCurrentPasswordFieldState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popup.Header onBack={onBack}>
|
||||||
|
{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
|
||||||
|
fluid
|
||||||
|
ref={passwordField}
|
||||||
|
name="password"
|
||||||
|
value={data.password}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
<div className={styles.text}>{t('common.currentPassword')}</div>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
ref={currentPasswordField}
|
||||||
|
type="password"
|
||||||
|
name="currentPassword"
|
||||||
|
value={data.currentPassword}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
positive
|
||||||
|
content={t('action.save')}
|
||||||
|
loading={isSubmitting}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Popup.Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
EditPasswordStep.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,
|
||||||
|
onBack: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditPasswordStep.defaultProps = {
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditPasswordStep;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
|
@ -2,23 +2,40 @@ import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Menu } from 'semantic-ui-react';
|
import { Menu } from 'semantic-ui-react';
|
||||||
import { withPopup } from '../../../lib/popup';
|
import { withPopup } from '../../lib/popup';
|
||||||
import { Popup } from '../../../lib/custom-ui';
|
import { Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useSteps } from '../../../hooks';
|
import { useSteps } from '../../hooks';
|
||||||
import EditNameStep from './EditNameStep';
|
import EditNameStep from './EditNameStep';
|
||||||
import EditAvatarStep from './EditAvatarStep';
|
import EditAvatarStep from './EditAvatarStep';
|
||||||
|
import EditEmailStep from './EditEmailStep';
|
||||||
|
import EditPasswordStep from './EditPasswordStep';
|
||||||
|
|
||||||
import styles from './UserPopup.module.css';
|
import styles from './UserPopup.module.css';
|
||||||
|
|
||||||
const StepTypes = {
|
const StepTypes = {
|
||||||
EDIT_NAME: 'EDIT_NAME',
|
EDIT_NAME: 'EDIT_NAME',
|
||||||
EDIT_AVATAR: 'EDIT_AVATAR',
|
EDIT_AVATAR: 'EDIT_AVATAR',
|
||||||
|
EDIT_EMAIL: 'EDIT_EMAIL',
|
||||||
|
EDIT_PASSWORD: 'EDIT_PASSWORD',
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserStep = React.memo(
|
const UserStep = React.memo(
|
||||||
({
|
({
|
||||||
name, avatar, isAvatarUploading, onUpdate, onAvatarUpload, onLogout, onClose,
|
email,
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
isAvatarUploading,
|
||||||
|
emailUpdateForm,
|
||||||
|
passwordUpdateForm,
|
||||||
|
onUpdate,
|
||||||
|
onAvatarUpload,
|
||||||
|
onEmailUpdate,
|
||||||
|
onEmailUpdateMessageDismiss,
|
||||||
|
onPasswordUpdate,
|
||||||
|
onPasswordUpdateMessageDismiss,
|
||||||
|
onLogout,
|
||||||
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
|
@ -31,6 +48,14 @@ const UserStep = React.memo(
|
||||||
openStep(StepTypes.EDIT_AVATAR);
|
openStep(StepTypes.EDIT_AVATAR);
|
||||||
}, [openStep]);
|
}, [openStep]);
|
||||||
|
|
||||||
|
const handleEmailEditClick = useCallback(() => {
|
||||||
|
openStep(StepTypes.EDIT_EMAIL);
|
||||||
|
}, [openStep]);
|
||||||
|
|
||||||
|
const handlePasswordEditClick = useCallback(() => {
|
||||||
|
openStep(StepTypes.EDIT_PASSWORD);
|
||||||
|
}, [openStep]);
|
||||||
|
|
||||||
const handleNameUpdate = useCallback(
|
const handleNameUpdate = useCallback(
|
||||||
(newName) => {
|
(newName) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
|
@ -68,6 +93,31 @@ const UserStep = React.memo(
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case StepTypes.EDIT_EMAIL:
|
||||||
|
return (
|
||||||
|
<EditEmailStep
|
||||||
|
defaultData={emailUpdateForm.data}
|
||||||
|
email={email}
|
||||||
|
isSubmitting={emailUpdateForm.isSubmitting}
|
||||||
|
error={emailUpdateForm.error}
|
||||||
|
onUpdate={onEmailUpdate}
|
||||||
|
onMessageDismiss={onEmailUpdateMessageDismiss}
|
||||||
|
onBack={handleBack}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case StepTypes.EDIT_PASSWORD:
|
||||||
|
return (
|
||||||
|
<EditPasswordStep
|
||||||
|
defaultData={passwordUpdateForm.data}
|
||||||
|
isSubmitting={passwordUpdateForm.isSubmitting}
|
||||||
|
error={passwordUpdateForm.error}
|
||||||
|
onUpdate={onPasswordUpdate}
|
||||||
|
onMessageDismiss={onPasswordUpdateMessageDismiss}
|
||||||
|
onBack={handleBack}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,6 +137,16 @@ const UserStep = React.memo(
|
||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item className={styles.menuItem} onClick={handleEmailEditClick}>
|
||||||
|
{t('action.editEmail', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item className={styles.menuItem} onClick={handlePasswordEditClick}>
|
||||||
|
{t('action.editPassword', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item className={styles.menuItem} onClick={onLogout}>
|
<Menu.Item className={styles.menuItem} onClick={onLogout}>
|
||||||
{t('action.logOut', {
|
{t('action.logOut', {
|
||||||
context: 'title',
|
context: 'title',
|
||||||
|
@ -100,11 +160,20 @@ const UserStep = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
UserStep.propTypes = {
|
UserStep.propTypes = {
|
||||||
|
email: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
avatar: PropTypes.string,
|
avatar: PropTypes.string,
|
||||||
isAvatarUploading: PropTypes.bool.isRequired,
|
isAvatarUploading: PropTypes.bool.isRequired,
|
||||||
|
/* eslint-disable react/forbid-prop-types */
|
||||||
|
emailUpdateForm: PropTypes.object.isRequired,
|
||||||
|
passwordUpdateForm: PropTypes.object.isRequired,
|
||||||
|
/* eslint-enable react/forbid-prop-types */
|
||||||
onUpdate: PropTypes.func.isRequired,
|
onUpdate: PropTypes.func.isRequired,
|
||||||
onAvatarUpload: PropTypes.func.isRequired,
|
onAvatarUpload: PropTypes.func.isRequired,
|
||||||
|
onEmailUpdate: PropTypes.func.isRequired,
|
||||||
|
onEmailUpdateMessageDismiss: PropTypes.func.isRequired,
|
||||||
|
onPasswordUpdate: PropTypes.func.isRequired,
|
||||||
|
onPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
|
@ -1,9 +1,7 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import { Button, Modal, Table } from 'semantic-ui-react';
|
||||||
Button, Modal, Table,
|
|
||||||
} from 'semantic-ui-react';
|
|
||||||
|
|
||||||
import AddUserPopupContainer from '../../containers/AddUserPopupContainer';
|
import AddUserPopupContainer from '../../containers/AddUserPopupContainer';
|
||||||
import Item from './Item';
|
import Item from './Item';
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
||||||
/* Login */
|
/* Login */
|
||||||
|
|
||||||
AUTHENTICATE: 'AUTHENTICATE',
|
AUTHENTICATE: 'AUTHENTICATE',
|
||||||
AUTHENTICATION_ERROR_CLEAR: 'AUTHENTICATION_ERROR_CLEAR',
|
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
|
||||||
LOGOUT: 'LOGOUT',
|
LOGOUT: 'LOGOUT',
|
||||||
AUTHENTICATE_REQUESTED: 'AUTHENTICATE_REQUESTED',
|
AUTHENTICATE_REQUESTED: 'AUTHENTICATE_REQUESTED',
|
||||||
AUTHENTICATE_SUCCEEDED: 'AUTHENTICATE_SUCCEEDED',
|
AUTHENTICATE_SUCCEEDED: 'AUTHENTICATE_SUCCEEDED',
|
||||||
|
@ -30,8 +30,10 @@ export default {
|
||||||
/* User */
|
/* User */
|
||||||
|
|
||||||
USER_CREATE: 'USER_CREATE',
|
USER_CREATE: 'USER_CREATE',
|
||||||
USER_CREATION_ERROR_CLEAR: 'USER_CREATION_ERROR_CLEAR',
|
USER_CREATE_ERROR_CLEAR: 'USER_CREATE_ERROR_CLEAR',
|
||||||
USER_UPDATE: 'USER_UPDATE',
|
USER_UPDATE: 'USER_UPDATE',
|
||||||
|
USER_EMAIL_UPDATE_ERROR_CLEAR: 'USER_EMAIL_UPDATE_ERROR_CLEAR',
|
||||||
|
USER_PASSWORD_UPDATE_ERROR_CLEAR: 'USER_PASSWORD_UPDATE_ERROR_CLEAR',
|
||||||
USER_DELETE: 'USER_DELETE',
|
USER_DELETE: 'USER_DELETE',
|
||||||
USER_TO_CARD_ADD: 'USER_TO_CARD_ADD',
|
USER_TO_CARD_ADD: 'USER_TO_CARD_ADD',
|
||||||
USER_FROM_CARD_REMOVE: 'USER_FROM_CARD_REMOVE',
|
USER_FROM_CARD_REMOVE: 'USER_FROM_CARD_REMOVE',
|
||||||
|
@ -48,6 +50,12 @@ export default {
|
||||||
USER_UPDATE_SUCCEEDED: 'USER_UPDATE_SUCCEEDED',
|
USER_UPDATE_SUCCEEDED: 'USER_UPDATE_SUCCEEDED',
|
||||||
USER_UPDATE_FAILED: 'USER_UPDATE_FAILED',
|
USER_UPDATE_FAILED: 'USER_UPDATE_FAILED',
|
||||||
USER_UPDATE_RECEIVED: 'USER_UPDATE_RECEIVED',
|
USER_UPDATE_RECEIVED: 'USER_UPDATE_RECEIVED',
|
||||||
|
USER_EMAIL_UPDATE_REQUESTED: 'USER_EMAIL_UPDATE_REQUESTED',
|
||||||
|
USER_EMAIL_UPDATE_SUCCEEDED: 'USER_EMAIL_UPDATE_SUCCEEDED',
|
||||||
|
USER_EMAIL_UPDATE_FAILED: 'USER_EMAIL_UPDATE_FAILED',
|
||||||
|
USER_PASSWORD_UPDATE_REQUESTED: 'USER_PASSWORD_UPDATE_REQUESTED',
|
||||||
|
USER_PASSWORD_UPDATE_SUCCEEDED: 'USER_PASSWORD_UPDATE_SUCCEEDED',
|
||||||
|
USER_PASSWORD_UPDATE_FAILED: 'USER_PASSWORD_UPDATE_FAILED',
|
||||||
USER_AVATAR_UPLOAD_REQUESTED: 'USER_AVATAR_UPLOAD_REQUESTED',
|
USER_AVATAR_UPLOAD_REQUESTED: 'USER_AVATAR_UPLOAD_REQUESTED',
|
||||||
USER_AVATAR_UPLOAD_SUCCEEDED: 'USER_AVATAR_UPLOAD_SUCCEEDED',
|
USER_AVATAR_UPLOAD_SUCCEEDED: 'USER_AVATAR_UPLOAD_SUCCEEDED',
|
||||||
USER_AVATAR_UPLOAD_FAILED: 'USER_AVATAR_UPLOAD_FAILED',
|
USER_AVATAR_UPLOAD_FAILED: 'USER_AVATAR_UPLOAD_FAILED',
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
||||||
/* Login */
|
/* Login */
|
||||||
|
|
||||||
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
|
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
|
||||||
AUTHENTICATION_ERROR_CLEAR: `${PREFIX}/AUTHENTICATION_ERROR_CLEAR`,
|
AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`,
|
||||||
LOGOUT: `${PREFIX}/LOGOUT`,
|
LOGOUT: `${PREFIX}/LOGOUT`,
|
||||||
|
|
||||||
/* Modal */
|
/* Modal */
|
||||||
|
@ -17,9 +17,13 @@ export default {
|
||||||
/* User */
|
/* User */
|
||||||
|
|
||||||
USER_CREATE: `${PREFIX}/USER_CREATE`,
|
USER_CREATE: `${PREFIX}/USER_CREATE`,
|
||||||
USER_CREATION_ERROR_CLEAR: `${PREFIX}/USER_CREATION_ERROR_CLEAR`,
|
USER_CREATE_ERROR_CLEAR: `${PREFIX}/USER_CREATE_ERROR_CLEAR`,
|
||||||
USER_UPDATE: `${PREFIX}/USER_UPDATE`,
|
USER_UPDATE: `${PREFIX}/USER_UPDATE`,
|
||||||
CURRENT_USER_UPDATE: `${PREFIX}/CURRENT_USER_UPDATE`,
|
CURRENT_USER_UPDATE: `${PREFIX}/CURRENT_USER_UPDATE`,
|
||||||
|
CURRENT_USER_EMAIL_UPDATE: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE`,
|
||||||
|
CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR`,
|
||||||
|
CURRENT_USER_PASSWORD_UPDATE: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE`,
|
||||||
|
CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR`,
|
||||||
CURRENT_USER_AVATAR_UPLOAD: `${PREFIX}/CURRENT_USER_AVATAR_UPLOAD`,
|
CURRENT_USER_AVATAR_UPLOAD: `${PREFIX}/CURRENT_USER_AVATAR_UPLOAD`,
|
||||||
USER_DELETE: `${PREFIX}/USER_DELETE`,
|
USER_DELETE: `${PREFIX}/USER_DELETE`,
|
||||||
USER_TO_CARD_ADD: `${PREFIX}/USER_TO_CARD_ADD`,
|
USER_TO_CARD_ADD: `${PREFIX}/USER_TO_CARD_ADD`,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||||
import { closeModal, createProject } from '../actions/entry';
|
import { closeModal, createProject } from '../actions/entry';
|
||||||
import AddProjectModal from '../components/AddProjectModal';
|
import AddProjectModal from '../components/AddProjectModal';
|
||||||
|
|
||||||
const mapStateToProps = ({ project: { data: defaultData, isSubmitting } }) => ({
|
const mapStateToProps = ({ projectCreateForm: { data: defaultData, isSubmitting } }) => ({
|
||||||
defaultData,
|
defaultData,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,36 +1,19 @@
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { clearUserCreationError, createUser } from '../actions/entry';
|
import { clearUserCreateError, createUser } from '../actions/entry';
|
||||||
import AddUserPopup from '../components/AddUserPopup';
|
import AddUserPopup from '../components/AddUserPopup';
|
||||||
|
|
||||||
const mapStateToProps = ({ user: { data: defaultData, isSubmitting, error: externalError } }) => {
|
const mapStateToProps = ({ userCreateForm: { data: defaultData, isSubmitting, error } }) => ({
|
||||||
let error;
|
defaultData,
|
||||||
|
isSubmitting,
|
||||||
if (externalError) {
|
error,
|
||||||
if (externalError.message === 'User is already exist') {
|
});
|
||||||
error = {
|
|
||||||
message: 'userIsAlreadyExist',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
error = {
|
|
||||||
type: 'warning',
|
|
||||||
message: 'unknownError',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
defaultData,
|
|
||||||
isSubmitting,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => bindActionCreators(
|
const mapDispatchToProps = (dispatch) => bindActionCreators(
|
||||||
{
|
{
|
||||||
onCreate: createUser,
|
onCreate: createUser,
|
||||||
onMessageDismiss: clearUserCreationError,
|
onMessageDismiss: clearUserCreateError,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,10 +3,14 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { currentUserSelector, notificationsForCurrentUserSelector } from '../selectors';
|
import { currentUserSelector, notificationsForCurrentUserSelector } from '../selectors';
|
||||||
import {
|
import {
|
||||||
|
clearCurrentUserEmailUpdateError,
|
||||||
|
clearCurrentUserPasswordUpdateError,
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
logout,
|
logout,
|
||||||
openUsersModal,
|
openUsersModal,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
|
updateCurrentUserEmail,
|
||||||
|
updateCurrentUserPassword,
|
||||||
uploadCurrentUserAvatar,
|
uploadCurrentUserAvatar,
|
||||||
} from '../actions/entry';
|
} from '../actions/entry';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
|
@ -24,10 +28,14 @@ const mapStateToProps = (state) => {
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => bindActionCreators(
|
const mapDispatchToProps = (dispatch) => bindActionCreators(
|
||||||
{
|
{
|
||||||
|
onUsers: openUsersModal, // TODO: rename
|
||||||
|
onNotificationDelete: deleteNotification,
|
||||||
onUserUpdate: updateCurrentUser,
|
onUserUpdate: updateCurrentUser,
|
||||||
onUserAvatarUpload: uploadCurrentUserAvatar,
|
onUserAvatarUpload: uploadCurrentUserAvatar,
|
||||||
onNotificationDelete: deleteNotification,
|
onUserEmailUpdate: updateCurrentUserEmail,
|
||||||
onUsers: openUsersModal, // TODO: rename
|
onUserEmailUpdateMessageDismiss: clearCurrentUserEmailUpdateError,
|
||||||
|
onUserPasswordUpdate: updateCurrentUserPassword,
|
||||||
|
onUserPasswordUpdateMessageDismiss: clearCurrentUserPasswordUpdateError,
|
||||||
onLogout: logout,
|
onLogout: logout,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
|
|
|
@ -1,59 +1,19 @@
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { authenticate, clearAuthenticationError } from '../actions/entry';
|
import { authenticate, clearAuthenticateError } from '../actions/entry';
|
||||||
import Login from '../components/Login';
|
import Login from '../components/Login';
|
||||||
|
|
||||||
const mapStateToProps = ({ login: { data: defaultData, isSubmitting, error: externalError } }) => {
|
const mapStateToProps = ({ authenticateForm: { data: defaultData, isSubmitting, error } }) => ({
|
||||||
let error;
|
defaultData,
|
||||||
|
isSubmitting,
|
||||||
if (externalError) {
|
error,
|
||||||
switch (externalError.message) {
|
});
|
||||||
case 'Email does not exist':
|
|
||||||
error = {
|
|
||||||
message: 'emailDoesNotExist',
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'Password is not valid':
|
|
||||||
error = {
|
|
||||||
message: 'invalidPassword',
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'Failed to fetch':
|
|
||||||
error = {
|
|
||||||
type: 'warning',
|
|
||||||
message: 'noInternetConnection',
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'Network request failed':
|
|
||||||
error = {
|
|
||||||
type: 'warning',
|
|
||||||
message: 'serverConnectionFailed',
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
error = {
|
|
||||||
type: 'warning',
|
|
||||||
message: 'unknownError',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
defaultData,
|
|
||||||
isSubmitting,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => bindActionCreators(
|
const mapDispatchToProps = (dispatch) => bindActionCreators(
|
||||||
{
|
{
|
||||||
onAuthenticate: authenticate,
|
onAuthenticate: authenticate,
|
||||||
onMessageDismiss: clearAuthenticationError,
|
onMessageDismiss: clearAuthenticateError,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import useDeepCompareEffect from './use-deep-compare-effect';
|
|
||||||
import useDeepCompareCallback from './use-deep-compare-callback';
|
|
||||||
import usePrevious from './use-previous';
|
import usePrevious from './use-previous';
|
||||||
import useField from './use-field';
|
import useField from './use-field';
|
||||||
import useForm from './use-form';
|
import useForm from './use-form';
|
||||||
|
@ -10,8 +8,6 @@ import useClosableForm from './use-closable-form';
|
||||||
import useDidUpdate from './use-did-update';
|
import useDidUpdate from './use-did-update';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useDeepCompareEffect,
|
|
||||||
useDeepCompareCallback,
|
|
||||||
usePrevious,
|
usePrevious,
|
||||||
useField,
|
useField,
|
||||||
useForm,
|
useForm,
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import useDeepCompareMemoize from './use-deep-compare-memoize';
|
|
||||||
|
|
||||||
export default (callback, dependencies) => useCallback(
|
|
||||||
callback,
|
|
||||||
useDeepCompareMemoize(dependencies),
|
|
||||||
);
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import useDeepCompareMemoize from './use-deep-compare-memoize';
|
|
||||||
|
|
||||||
export default (effect, dependencies) => {
|
|
||||||
useEffect(effect, useDeepCompareMemoize(dependencies));
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
import dequal from 'dequal';
|
|
||||||
import { useRef } from 'react';
|
|
||||||
|
|
||||||
export default (value) => {
|
|
||||||
const currentValue = useRef();
|
|
||||||
|
|
||||||
if (!dequal(value, currentValue.current)) {
|
|
||||||
currentValue.current = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentValue.current;
|
|
||||||
};
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export default (value) => {
|
export default (value) => {
|
||||||
const prevValue = useRef();
|
const prevValue = useRef(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
prevValue.current = value;
|
prevValue.current = value;
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default {
|
||||||
createLabel_title: 'Create Label',
|
createLabel_title: 'Create Label',
|
||||||
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one',
|
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one',
|
||||||
createProject_title: 'Create Project',
|
createProject_title: 'Create Project',
|
||||||
|
currentPassword: 'Current password',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
dueDate: 'Due date',
|
dueDate: 'Due date',
|
||||||
deleteBoard_title: 'Delete Board',
|
deleteBoard_title: 'Delete Board',
|
||||||
|
@ -49,8 +50,10 @@ export default {
|
||||||
editAvatar_title: 'Edit Avatar',
|
editAvatar_title: 'Edit Avatar',
|
||||||
editBoard_title: 'Edit Board',
|
editBoard_title: 'Edit Board',
|
||||||
editDueDate_title: 'Edit Due Date',
|
editDueDate_title: 'Edit Due Date',
|
||||||
|
editEmail_title: 'Edit E-mail',
|
||||||
editLabel_title: 'Edit Label',
|
editLabel_title: 'Edit Label',
|
||||||
editName_title: 'Edit Name',
|
editName_title: 'Edit Name',
|
||||||
|
editPassword_title: 'Edit Password',
|
||||||
editProject_title: 'Edit Project',
|
editProject_title: 'Edit Project',
|
||||||
editTimer_title: 'Edit Timer',
|
editTimer_title: 'Edit Timer',
|
||||||
enterCardTitle: 'Enter card title...',
|
enterCardTitle: 'Enter card title...',
|
||||||
|
@ -61,11 +64,14 @@ export default {
|
||||||
filterByLabels_title: 'Filter By Labels',
|
filterByLabels_title: 'Filter By Labels',
|
||||||
filterByMembers_title: 'Filter By Members',
|
filterByMembers_title: 'Filter By Members',
|
||||||
hours: 'Hours',
|
hours: 'Hours',
|
||||||
|
invalidCurrentPassword: 'Invalid current password',
|
||||||
labels: 'Labels',
|
labels: 'Labels',
|
||||||
listActions_title: 'List Actions',
|
listActions_title: 'List Actions',
|
||||||
members: 'Members',
|
members: 'Members',
|
||||||
minutes: 'Minutes',
|
minutes: 'Minutes',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
|
newEmail: 'New e-mail',
|
||||||
|
newPassword: 'New password',
|
||||||
noConnectionToServer: 'No connection to server',
|
noConnectionToServer: 'No connection to server',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
noUnreadNotifications: 'No unread notifications',
|
noUnreadNotifications: 'No unread notifications',
|
||||||
|
@ -121,7 +127,9 @@ export default {
|
||||||
editAvatar_title: 'Edit Avatar',
|
editAvatar_title: 'Edit Avatar',
|
||||||
editDueDate_title: 'Edit Due Date',
|
editDueDate_title: 'Edit Due Date',
|
||||||
editDescription_title: 'Edit Description',
|
editDescription_title: 'Edit Description',
|
||||||
|
editEmail_title: 'Edit E-mail',
|
||||||
editName_title: 'Edit Name',
|
editName_title: 'Edit Name',
|
||||||
|
editPassword_title: 'Edit Password',
|
||||||
editTask_title: 'Edit Task',
|
editTask_title: 'Edit Task',
|
||||||
editTimer_title: 'Edit Timer',
|
editTimer_title: 'Edit Timer',
|
||||||
editTitle_title: 'Edit Title',
|
editTitle_title: 'Edit Title',
|
||||||
|
|
|
@ -2,7 +2,7 @@ export default {
|
||||||
translation: {
|
translation: {
|
||||||
common: {
|
common: {
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
emailDoesNotExist: 'Email does not exist',
|
emailDoesNotExist: 'E-mail does not exist',
|
||||||
invalidPassword: 'Invalid password',
|
invalidPassword: 'Invalid password',
|
||||||
logInToPlanka: 'Log in to Planka',
|
logInToPlanka: 'Log in to Planka',
|
||||||
noInternetConnection: 'No internet connection',
|
noInternetConnection: 'No internet connection',
|
||||||
|
|
|
@ -39,6 +39,7 @@ export default {
|
||||||
createLabel: 'Создание метки',
|
createLabel: 'Создание метки',
|
||||||
createNewOneOrSelectExistingOne: 'Создайте новую или выберите<br />уже существующую',
|
createNewOneOrSelectExistingOne: 'Создайте новую или выберите<br />уже существующую',
|
||||||
createProject: 'Создание проекта',
|
createProject: 'Создание проекта',
|
||||||
|
currentPassword: 'Текущий пароль',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
dueDate: 'Срок',
|
dueDate: 'Срок',
|
||||||
deleteBoard: 'Удаление доски',
|
deleteBoard: 'Удаление доски',
|
||||||
|
@ -53,8 +54,10 @@ export default {
|
||||||
editAvatar: 'Изменение аватара',
|
editAvatar: 'Изменение аватара',
|
||||||
editBoard: 'Изменение доски',
|
editBoard: 'Изменение доски',
|
||||||
editDueDate: 'Изменение срока',
|
editDueDate: 'Изменение срока',
|
||||||
|
editEmail: 'Изменение e-mail',
|
||||||
editLabel: 'Изменения метки',
|
editLabel: 'Изменения метки',
|
||||||
editName: 'Изменение имени',
|
editName: 'Изменение имени',
|
||||||
|
editPassword: 'Изменение пароля',
|
||||||
editProject: 'Изменение проекта',
|
editProject: 'Изменение проекта',
|
||||||
editTimer: 'Изменение таймера',
|
editTimer: 'Изменение таймера',
|
||||||
enterCardTitle: 'Введите заголовок для этой карточки...',
|
enterCardTitle: 'Введите заголовок для этой карточки...',
|
||||||
|
@ -65,11 +68,14 @@ export default {
|
||||||
filterByLabels: 'Фильтр по меткам',
|
filterByLabels: 'Фильтр по меткам',
|
||||||
filterByMembers: 'Фильтр по участникам',
|
filterByMembers: 'Фильтр по участникам',
|
||||||
hours: 'Часы',
|
hours: 'Часы',
|
||||||
|
invalidCurrentPassword: 'Неверный текущий пароль',
|
||||||
labels: 'Метки',
|
labels: 'Метки',
|
||||||
listActions: 'Действия со списком',
|
listActions: 'Действия со списком',
|
||||||
members: 'Участники',
|
members: 'Участники',
|
||||||
minutes: 'Минуты',
|
minutes: 'Минуты',
|
||||||
name: 'Имя',
|
name: 'Имя',
|
||||||
|
newEmail: 'Новый e-mail',
|
||||||
|
newPassword: 'Новый пароль',
|
||||||
noConnectionToServer: 'Нет соединения с сервером',
|
noConnectionToServer: 'Нет соединения с сервером',
|
||||||
notifications: 'Уведомления',
|
notifications: 'Уведомления',
|
||||||
noUnreadNotifications: 'Уведомлений нет',
|
noUnreadNotifications: 'Уведомлений нет',
|
||||||
|
@ -121,7 +127,9 @@ export default {
|
||||||
editAvatar: 'Изменить аватар',
|
editAvatar: 'Изменить аватар',
|
||||||
editDueDate: 'Изменить срок',
|
editDueDate: 'Изменить срок',
|
||||||
editDescription: 'Изменить описание',
|
editDescription: 'Изменить описание',
|
||||||
|
editEmail: 'Изменить e-mail',
|
||||||
editName: 'Изменить имя',
|
editName: 'Изменить имя',
|
||||||
|
editPassword: 'Изменить пароль',
|
||||||
editTask: 'Изменить задачу',
|
editTask: 'Изменить задачу',
|
||||||
editTimer: 'Изменить таймер',
|
editTimer: 'Изменить таймер',
|
||||||
editTitle: 'Изменить название',
|
editTitle: 'Изменить название',
|
||||||
|
|
|
@ -2,6 +2,24 @@ import { Model, attr } from 'redux-orm';
|
||||||
|
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
const DEFAULT_EMAIL_UPDATE_FORM = {
|
||||||
|
data: {
|
||||||
|
email: '',
|
||||||
|
currentPassword: '',
|
||||||
|
},
|
||||||
|
isSubmitting: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PASSWORD_UPDATE_FORM = {
|
||||||
|
data: {
|
||||||
|
password: '',
|
||||||
|
currentPassword: '',
|
||||||
|
},
|
||||||
|
isSubmitting: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
export default class extends Model {
|
export default class extends Model {
|
||||||
static modelName = 'User';
|
static modelName = 'User';
|
||||||
|
|
||||||
|
@ -17,6 +35,12 @@ export default class extends Model {
|
||||||
isAvatarUploading: attr({
|
isAvatarUploading: attr({
|
||||||
getDefault: () => false,
|
getDefault: () => false,
|
||||||
}),
|
}),
|
||||||
|
emailUpdateForm: attr({
|
||||||
|
getDefault: () => DEFAULT_EMAIL_UPDATE_FORM,
|
||||||
|
}),
|
||||||
|
passwordUpdateForm: attr({
|
||||||
|
getDefault: () => DEFAULT_PASSWORD_UPDATE_FORM,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
static reducer({ type, payload }, User) {
|
static reducer({ type, payload }, User) {
|
||||||
|
@ -44,6 +68,30 @@ export default class extends Model {
|
||||||
User.withId(payload.id).update(payload.data);
|
User.withId(payload.id).update(payload.data);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case ActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR: {
|
||||||
|
const userModel = User.withId(payload.id);
|
||||||
|
|
||||||
|
userModel.update({
|
||||||
|
emailUpdateForm: {
|
||||||
|
...userModel.emailUpdateForm,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR: {
|
||||||
|
const userModel = User.withId(payload.id);
|
||||||
|
|
||||||
|
userModel.update({
|
||||||
|
passwordUpdateForm: {
|
||||||
|
...userModel.passwordUpdateForm,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ActionTypes.USER_DELETE:
|
case ActionTypes.USER_DELETE:
|
||||||
User.withId(payload.id).deleteWithRelated();
|
User.withId(payload.id).deleteWithRelated();
|
||||||
|
|
||||||
|
@ -52,6 +100,73 @@ export default class extends Model {
|
||||||
User.withId(payload.user.id).update(payload.user);
|
User.withId(payload.user.id).update(payload.user);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case ActionTypes.USER_EMAIL_UPDATE_REQUESTED: {
|
||||||
|
const userModel = User.withId(payload.id);
|
||||||
|
|
||||||
|
userModel.update({
|
||||||
|
emailUpdateForm: {
|
||||||
|
...userModel.emailUpdateForm,
|
||||||
|
data: payload.data,
|
||||||
|
isSubmitting: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActionTypes.USER_EMAIL_UPDATE_SUCCEEDED: {
|
||||||
|
User.withId(payload.id).update({
|
||||||
|
email: payload.email,
|
||||||
|
emailUpdateForm: DEFAULT_EMAIL_UPDATE_FORM,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActionTypes.USER_EMAIL_UPDATE_FAILED: {
|
||||||
|
const userModel = User.withId(payload.id);
|
||||||
|
|
||||||
|
userModel.update({
|
||||||
|
emailUpdateForm: {
|
||||||
|
...userModel.emailUpdateForm,
|
||||||
|
isSubmitting: false,
|
||||||
|
error: payload.error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActionTypes.USER_PASSWORD_UPDATE_REQUESTED: {
|
||||||
|
const userModel = User.withId(payload.id);
|
||||||
|
|
||||||
|
userModel.update({
|
||||||
|
passwordUpdateForm: {
|
||||||
|
...userModel.passwordUpdateForm,
|
||||||
|
data: payload.data,
|
||||||
|
isSubmitting: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActionTypes.USER_PASSWORD_UPDATE_SUCCEEDED: {
|
||||||
|
User.withId(payload.id).update({
|
||||||
|
passwordUpdateForm: DEFAULT_PASSWORD_UPDATE_FORM,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActionTypes.USER_PASSWORD_UPDATE_FAILED: {
|
||||||
|
const userModel = User.withId(payload.id);
|
||||||
|
|
||||||
|
userModel.update({
|
||||||
|
passwordUpdateForm: {
|
||||||
|
...userModel.passwordUpdateForm,
|
||||||
|
isSubmitting: false,
|
||||||
|
error: payload.error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ActionTypes.USER_AVATAR_UPLOAD_REQUESTED:
|
case ActionTypes.USER_AVATAR_UPLOAD_REQUESTED:
|
||||||
User.withId(payload.id).update({
|
User.withId(payload.id).update({
|
||||||
isAvatarUploading: true,
|
isAvatarUploading: true,
|
||||||
|
@ -59,8 +174,8 @@ export default class extends Model {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED:
|
case ActionTypes.USER_AVATAR_UPLOAD_SUCCEEDED:
|
||||||
User.withId(payload.user.id).update({
|
User.withId(payload.id).update({
|
||||||
...payload.user,
|
avatar: payload.avatar,
|
||||||
isAvatarUploading: false,
|
isAvatarUploading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
} from './models';
|
} from './models';
|
||||||
|
|
||||||
const orm = new ORM({
|
const orm = new ORM({
|
||||||
stateSelector: ({ db }) => db,
|
stateSelector: (state) => state.orm,
|
||||||
});
|
});
|
||||||
|
|
||||||
orm.register(
|
orm.register(
|
||||||
|
|
4
client/src/reducers/login.js → client/src/reducers/forms/authenticate.js
Executable file → Normal file
4
client/src/reducers/login.js → client/src/reducers/forms/authenticate.js
Executable file → Normal file
|
@ -1,4 +1,4 @@
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
data: {
|
data: {
|
||||||
|
@ -19,7 +19,7 @@ export default (state = initialState, { type, payload }) => {
|
||||||
...payload.data,
|
...payload.data,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
case ActionTypes.AUTHENTICATION_ERROR_CLEAR:
|
case ActionTypes.AUTHENTICATE_ERROR_CLEAR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: null,
|
error: null,
|
11
client/src/reducers/forms/index.js
Normal file
11
client/src/reducers/forms/index.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
|
import authenticate from './authenticate';
|
||||||
|
import userCreate from './user-create';
|
||||||
|
import projectCreate from './project-create';
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
authenticate,
|
||||||
|
userCreate,
|
||||||
|
projectCreate,
|
||||||
|
});
|
2
client/src/reducers/project.js → client/src/reducers/forms/project-create.js
Executable file → Normal file
2
client/src/reducers/project.js → client/src/reducers/forms/project-create.js
Executable file → Normal file
|
@ -1,4 +1,4 @@
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
data: {
|
data: {
|
4
client/src/reducers/user.js → client/src/reducers/forms/user-create.js
Executable file → Normal file
4
client/src/reducers/user.js → client/src/reducers/forms/user-create.js
Executable file → Normal file
|
@ -1,4 +1,4 @@
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
data: {
|
data: {
|
||||||
|
@ -19,7 +19,7 @@ export default (state = initialState, { type, payload }) => {
|
||||||
...payload.data,
|
...payload.data,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
case ActionTypes.USER_CREATION_ERROR_CLEAR:
|
case ActionTypes.USER_CREATE_ERROR_CLEAR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: null,
|
error: null,
|
|
@ -2,20 +2,20 @@ import { combineReducers } from 'redux';
|
||||||
|
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import socket from './socket';
|
import socket from './socket';
|
||||||
import db from './db';
|
import orm from './orm';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import login from './login';
|
|
||||||
import app from './app';
|
import app from './app';
|
||||||
import user from './user';
|
import authenticateForm from './forms/authenticate';
|
||||||
import project from './project';
|
import userCreateForm from './forms/user-create';
|
||||||
|
import projectCreateForm from './forms/project-create';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
router,
|
router,
|
||||||
socket,
|
socket,
|
||||||
db,
|
orm,
|
||||||
auth,
|
auth,
|
||||||
login,
|
|
||||||
app,
|
app,
|
||||||
user,
|
authenticateForm,
|
||||||
project,
|
userCreateForm,
|
||||||
|
projectCreateForm,
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,13 @@ import {
|
||||||
fetchCurrentUserFailed,
|
fetchCurrentUserFailed,
|
||||||
fetchCurrentUserRequested,
|
fetchCurrentUserRequested,
|
||||||
fetchCurrentUserSucceeded,
|
fetchCurrentUserSucceeded,
|
||||||
|
updateUserEmailFailed,
|
||||||
|
updateUserEmailRequested,
|
||||||
|
updateUserEmailSucceeded,
|
||||||
updateUserFailed,
|
updateUserFailed,
|
||||||
|
updateUserPasswordFailed,
|
||||||
|
updateUserPasswordRequested,
|
||||||
|
updateUserPasswordSucceeded,
|
||||||
updateUserRequested,
|
updateUserRequested,
|
||||||
updateUserSucceeded,
|
updateUserSucceeded,
|
||||||
uploadUserAvatarFailed,
|
uploadUserAvatarFailed,
|
||||||
|
@ -92,13 +98,61 @@ export function* updateUserRequest(id, data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* updateUserEmailRequest(id, data) {
|
||||||
|
yield put(updateUserEmailRequested(id, data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { item } = yield call(request, api.updateUserEmail, id, data);
|
||||||
|
|
||||||
|
const action = updateUserEmailSucceeded(id, item);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const action = updateUserEmailFailed(id, error);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* updateUserPasswordRequest(id, data) {
|
||||||
|
yield put(updateUserPasswordRequested(id, data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield call(request, api.updateUserPassword, id, data);
|
||||||
|
|
||||||
|
const action = updateUserPasswordSucceeded(id);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const action = updateUserPasswordFailed(id, error);
|
||||||
|
yield put(action);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
payload: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function* uploadUserAvatarRequest(id, file) {
|
export function* uploadUserAvatarRequest(id, file) {
|
||||||
yield put(uploadUserAvatarRequested(id));
|
yield put(uploadUserAvatarRequested(id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { item } = yield call(request, api.uploadUserAvatar, id, file);
|
const { item } = yield call(request, api.uploadUserAvatar, id, file);
|
||||||
|
|
||||||
const action = uploadUserAvatarSucceeded(item);
|
const action = uploadUserAvatarSucceeded(id, item);
|
||||||
yield put(action);
|
yield put(action);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
createUserRequest,
|
createUserRequest,
|
||||||
deleteCardMembershipRequest,
|
deleteCardMembershipRequest,
|
||||||
deleteUserRequest,
|
deleteUserRequest,
|
||||||
|
updateUserEmailRequest,
|
||||||
|
updateUserPasswordRequest,
|
||||||
updateUserRequest,
|
updateUserRequest,
|
||||||
uploadUserAvatarRequest,
|
uploadUserAvatarRequest,
|
||||||
} from '../requests';
|
} from '../requests';
|
||||||
|
@ -12,7 +14,9 @@ import { currentUserIdSelector, pathSelector } from '../../../selectors';
|
||||||
import {
|
import {
|
||||||
addUserToBoardFilter,
|
addUserToBoardFilter,
|
||||||
addUserToCard,
|
addUserToCard,
|
||||||
clearUserCreationError,
|
clearUserCreateError,
|
||||||
|
clearUserEmailUpdateError,
|
||||||
|
clearUserPasswordUpdateError,
|
||||||
createUser,
|
createUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
|
@ -25,8 +29,8 @@ export function* createUserService(data) {
|
||||||
yield call(createUserRequest, data);
|
yield call(createUserRequest, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* clearUserCreationErrorService() {
|
export function* clearUserCreateErrorService() {
|
||||||
yield put(clearUserCreationError());
|
yield put(clearUserCreateError());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* updateUserService(id, data) {
|
export function* updateUserService(id, data) {
|
||||||
|
@ -40,10 +44,54 @@ export function* updateCurrentUserService(data) {
|
||||||
yield call(updateUserService, id, data);
|
yield call(updateUserService, id, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* updateUserEmailService(id, data) {
|
||||||
|
yield call(updateUserEmailRequest, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* updateCurrentUserEmailService(data) {
|
||||||
|
const id = yield select(currentUserIdSelector);
|
||||||
|
|
||||||
|
yield call(updateUserEmailService, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* clearUserEmailUpdateErrorService(id) {
|
||||||
|
yield put(clearUserEmailUpdateError(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* clearCurrentUserEmailUpdateErrorService() {
|
||||||
|
const id = yield select(currentUserIdSelector);
|
||||||
|
|
||||||
|
yield call(clearUserEmailUpdateErrorService, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* updateUserPasswordService(id, data) {
|
||||||
|
yield call(updateUserPasswordRequest, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* updateCurrentUserPasswordService(data) {
|
||||||
|
const id = yield select(currentUserIdSelector);
|
||||||
|
|
||||||
|
yield call(updateUserPasswordService, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* clearUserPasswordUpdateErrorService(id) {
|
||||||
|
yield put(clearUserPasswordUpdateError(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* clearCurrentUserPasswordUpdateErrorService() {
|
||||||
|
const id = yield select(currentUserIdSelector);
|
||||||
|
|
||||||
|
yield call(clearUserPasswordUpdateErrorService, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* uploadUserAvatarService(id, file) {
|
||||||
|
yield call(uploadUserAvatarRequest, id, file);
|
||||||
|
}
|
||||||
|
|
||||||
export function* uploadCurrentUserAvatarService(file) {
|
export function* uploadCurrentUserAvatarService(file) {
|
||||||
const id = yield select(currentUserIdSelector);
|
const id = yield select(currentUserIdSelector);
|
||||||
|
|
||||||
yield call(uploadUserAvatarRequest, id, file);
|
yield call(uploadUserAvatarService, id, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* deleteUserService(id) {
|
export function* deleteUserService(id) {
|
||||||
|
|
|
@ -4,13 +4,17 @@ import {
|
||||||
addUserToCardService,
|
addUserToCardService,
|
||||||
addUserToCurrentCardService,
|
addUserToCurrentCardService,
|
||||||
addUserToFilterInCurrentBoardService,
|
addUserToFilterInCurrentBoardService,
|
||||||
clearUserCreationErrorService,
|
clearCurrentUserEmailUpdateErrorService,
|
||||||
|
clearCurrentUserPasswordUpdateErrorService,
|
||||||
|
clearUserCreateErrorService,
|
||||||
createUserService,
|
createUserService,
|
||||||
deleteUserService,
|
deleteUserService,
|
||||||
removeUserFromCardService,
|
removeUserFromCardService,
|
||||||
removeUserFromCurrentCardService,
|
removeUserFromCurrentCardService,
|
||||||
removeUserFromFilterInCurrentBoardService,
|
removeUserFromFilterInCurrentBoardService,
|
||||||
updateUserService,
|
updateUserService,
|
||||||
|
updateCurrentUserEmailService,
|
||||||
|
updateCurrentUserPasswordService,
|
||||||
updateCurrentUserService,
|
updateCurrentUserService,
|
||||||
uploadCurrentUserAvatarService,
|
uploadCurrentUserAvatarService,
|
||||||
} from '../services';
|
} from '../services';
|
||||||
|
@ -19,7 +23,7 @@ import EntryActionTypes from '../../../constants/EntryActionTypes';
|
||||||
export default function* () {
|
export default function* () {
|
||||||
yield all([
|
yield all([
|
||||||
takeLatest(EntryActionTypes.USER_CREATE, ({ payload: { data } }) => createUserService(data)),
|
takeLatest(EntryActionTypes.USER_CREATE, ({ payload: { data } }) => createUserService(data)),
|
||||||
takeLatest(EntryActionTypes.USER_CREATION_ERROR_CLEAR, () => clearUserCreationErrorService()),
|
takeLatest(EntryActionTypes.USER_CREATE_ERROR_CLEAR, () => clearUserCreateErrorService()),
|
||||||
takeLatest(
|
takeLatest(
|
||||||
EntryActionTypes.USER_UPDATE,
|
EntryActionTypes.USER_UPDATE,
|
||||||
({ payload: { id, data } }) => updateUserService(id, data),
|
({ payload: { id, data } }) => updateUserService(id, data),
|
||||||
|
@ -28,6 +32,22 @@ export default function* () {
|
||||||
EntryActionTypes.CURRENT_USER_UPDATE,
|
EntryActionTypes.CURRENT_USER_UPDATE,
|
||||||
({ payload: { data } }) => updateCurrentUserService(data),
|
({ payload: { data } }) => updateCurrentUserService(data),
|
||||||
),
|
),
|
||||||
|
takeLatest(
|
||||||
|
EntryActionTypes.CURRENT_USER_EMAIL_UPDATE,
|
||||||
|
({ payload: { data } }) => updateCurrentUserEmailService(data),
|
||||||
|
),
|
||||||
|
takeLatest(
|
||||||
|
EntryActionTypes.CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR,
|
||||||
|
() => clearCurrentUserEmailUpdateErrorService(),
|
||||||
|
),
|
||||||
|
takeLatest(
|
||||||
|
EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE,
|
||||||
|
({ payload: { data } }) => updateCurrentUserPasswordService(data),
|
||||||
|
),
|
||||||
|
takeLatest(
|
||||||
|
EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR,
|
||||||
|
() => clearCurrentUserPasswordUpdateErrorService(),
|
||||||
|
),
|
||||||
takeLatest(
|
takeLatest(
|
||||||
EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD,
|
EntryActionTypes.CURRENT_USER_AVATAR_UPLOAD,
|
||||||
({ payload: { file } }) => uploadCurrentUserAvatarService(file),
|
({ payload: { file } }) => uploadCurrentUserAvatarService(file),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { call, put } from 'redux-saga/effects';
|
import { call, put } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { authenticateRequest } from '../requests';
|
import { authenticateRequest } from '../requests';
|
||||||
import { authenticate, clearAuthenticationError } from '../../../actions';
|
import { authenticate, clearAuthenticateError } from '../../../actions';
|
||||||
|
|
||||||
export function* authenticateService(data) {
|
export function* authenticateService(data) {
|
||||||
yield put(authenticate(data));
|
yield put(authenticate(data));
|
||||||
yield call(authenticateRequest, data);
|
yield call(authenticateRequest, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* clearAuthenticationErrorService() {
|
export function* clearAuthenticateErrorService() {
|
||||||
yield put(clearAuthenticationError());
|
yield put(clearAuthenticateError());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { all, takeLatest } from 'redux-saga/effects';
|
import { all, takeLatest } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { authenticateService, clearAuthenticationErrorService } from '../services';
|
import { authenticateService, clearAuthenticateErrorService } from '../services';
|
||||||
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
import EntryActionTypes from '../../../constants/EntryActionTypes';
|
||||||
|
|
||||||
export default function* () {
|
export default function* () {
|
||||||
yield all([
|
yield all([
|
||||||
takeLatest(EntryActionTypes.AUTHENTICATE, ({ payload: { data } }) => authenticateService(data)),
|
takeLatest(EntryActionTypes.AUTHENTICATE, ({ payload: { data } }) => authenticateService(data)),
|
||||||
takeLatest(
|
takeLatest(EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, () => clearAuthenticateErrorService()),
|
||||||
EntryActionTypes.AUTHENTICATION_ERROR_CLEAR,
|
|
||||||
() => clearAuthenticationErrorService(),
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
83
server/api/controllers/users/update-email.js
Normal file
83
server/api/controllers/users/update-email.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
|
const Errors = {
|
||||||
|
USER_NOT_FOUND: {
|
||||||
|
notFound: 'User is not found'
|
||||||
|
},
|
||||||
|
CURRENT_PASSWORD_NOT_VALID: {
|
||||||
|
forbidden: 'Current password is not valid'
|
||||||
|
},
|
||||||
|
USER_EXIST: {
|
||||||
|
conflict: 'User is already exist'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
regex: /^[0-9]+$/,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
isEmail: true,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
currentPassword: {
|
||||||
|
type: 'string',
|
||||||
|
isNotEmptyString: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
notFound: {
|
||||||
|
responseType: 'notFound'
|
||||||
|
},
|
||||||
|
forbidden: {
|
||||||
|
responseType: 'forbidden'
|
||||||
|
},
|
||||||
|
conflict: {
|
||||||
|
responseType: 'conflict'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fn: async function(inputs, exits) {
|
||||||
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
|
if (inputs.id === currentUser.id) {
|
||||||
|
if (!inputs.currentPassword) {
|
||||||
|
throw Errors.CURRENT_PASSWORD_NOT_VALID;
|
||||||
|
}
|
||||||
|
} else if (!currentUser.isAdmin) {
|
||||||
|
throw Errors.USER_NOT_FOUND; // Forbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await sails.helpers.getUser(inputs.id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
inputs.id === currentUser.id &&
|
||||||
|
!bcrypt.compareSync(inputs.currentPassword, user.password)
|
||||||
|
) {
|
||||||
|
throw Errors.CURRENT_PASSWORD_NOT_VALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = _.pick(inputs, ['email']);
|
||||||
|
|
||||||
|
user = await sails.helpers
|
||||||
|
.updateUser(user, values, this.req)
|
||||||
|
.intercept('conflict', () => Errors.USER_EXIST);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: user.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
74
server/api/controllers/users/update-password.js
Normal file
74
server/api/controllers/users/update-password.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
|
||||||
|
const Errors = {
|
||||||
|
USER_NOT_FOUND: {
|
||||||
|
notFound: 'User is not found'
|
||||||
|
},
|
||||||
|
CURRENT_PASSWORD_NOT_VALID: {
|
||||||
|
forbidden: 'Current password is not valid'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inputs: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
regex: /^[0-9]+$/,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
currentPassword: {
|
||||||
|
type: 'string',
|
||||||
|
isNotEmptyString: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
notFound: {
|
||||||
|
responseType: 'notFound'
|
||||||
|
},
|
||||||
|
forbidden: {
|
||||||
|
responseType: 'forbidden'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fn: async function(inputs, exits) {
|
||||||
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
|
if (inputs.id === currentUser.id) {
|
||||||
|
if (!inputs.currentPassword) {
|
||||||
|
throw Errors.CURRENT_PASSWORD_NOT_VALID;
|
||||||
|
}
|
||||||
|
} else if (!currentUser.isAdmin) {
|
||||||
|
throw Errors.USER_NOT_FOUND; // Forbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await sails.helpers.getUser(inputs.id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
inputs.id === currentUser.id &&
|
||||||
|
!bcrypt.compareSync(inputs.currentPassword, user.password)
|
||||||
|
) {
|
||||||
|
throw Errors.CURRENT_PASSWORD_NOT_VALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = _.pick(inputs, ['password']);
|
||||||
|
|
||||||
|
user = await sails.helpers.updateUser(user, values, this.req);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw Errors.USER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exits.success({
|
||||||
|
item: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -118,7 +118,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.res.json({
|
return this.res.json({
|
||||||
item: user
|
item: user.avatar
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,19 +21,31 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
exits: {
|
||||||
|
conflict: {}
|
||||||
|
},
|
||||||
|
|
||||||
fn: async function(inputs, exits) {
|
fn: async function(inputs, exits) {
|
||||||
if (!_.isUndefined(inputs.values.email)) {
|
if (!_.isUndefined(inputs.values.email)) {
|
||||||
inputs.values.email = inputs.values.email.toLowerCase();
|
inputs.values.email = inputs.values.email.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isOnlyPasswordChange = false;
|
||||||
|
|
||||||
if (!_.isUndefined(inputs.values.password)) {
|
if (!_.isUndefined(inputs.values.password)) {
|
||||||
inputs.values.password = bcrypt.hashSync(inputs.values.password, 10);
|
inputs.values.password = bcrypt.hashSync(inputs.values.password, 10);
|
||||||
|
|
||||||
|
if (Object.keys(inputs.values).length === 1) {
|
||||||
|
isOnlyPasswordChange = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.updateOne({
|
const user = await User.updateOne({
|
||||||
id: inputs.record.id,
|
id: inputs.record.id,
|
||||||
deletedAt: null
|
deletedAt: null
|
||||||
}).set(inputs.values);
|
})
|
||||||
|
.set(inputs.values)
|
||||||
|
.intercept(undefined, 'conflict');
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (inputs.record.avatar && user.avatar !== inputs.record.avatar) {
|
if (inputs.record.avatar && user.avatar !== inputs.record.avatar) {
|
||||||
|
@ -44,28 +56,30 @@ module.exports = {
|
||||||
} catch (unusedError) {}
|
} catch (unusedError) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminUserIds = await sails.helpers.getAdminUserIds();
|
if (!isOnlyPasswordChange) {
|
||||||
|
const adminUserIds = await sails.helpers.getAdminUserIds();
|
||||||
|
|
||||||
const projectIds = await sails.helpers.getMembershipProjectIdsForUser(
|
const projectIds = await sails.helpers.getMembershipProjectIdsForUser(
|
||||||
user.id
|
user.id
|
||||||
);
|
|
||||||
|
|
||||||
const userIdsForProject = await sails.helpers.getMembershipUserIdsForProject(
|
|
||||||
projectIds
|
|
||||||
);
|
|
||||||
|
|
||||||
const userIds = _.union([user.id], adminUserIds, userIdsForProject);
|
|
||||||
|
|
||||||
userIds.forEach(userId => {
|
|
||||||
sails.sockets.broadcast(
|
|
||||||
`user:${userId}`,
|
|
||||||
'userUpdate',
|
|
||||||
{
|
|
||||||
item: user
|
|
||||||
},
|
|
||||||
inputs.request
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
const userIdsForProject = await sails.helpers.getMembershipUserIdsForProject(
|
||||||
|
projectIds
|
||||||
|
);
|
||||||
|
|
||||||
|
const userIds = _.union([user.id], adminUserIds, userIdsForProject);
|
||||||
|
|
||||||
|
userIds.forEach(userId => {
|
||||||
|
sails.sockets.broadcast(
|
||||||
|
`user:${userId}`,
|
||||||
|
'userUpdate',
|
||||||
|
{
|
||||||
|
item: user
|
||||||
|
},
|
||||||
|
inputs.request
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return exits.success(user);
|
return exits.success(user);
|
||||||
|
|
36
server/api/responses/forbidden.js
Normal file
36
server/api/responses/forbidden.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* forbidden.js
|
||||||
|
*
|
||||||
|
* A custom response.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* ```
|
||||||
|
* return res.forbidden();
|
||||||
|
* // -or-
|
||||||
|
* return res.forbidden(optionalData);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Or with actions2:
|
||||||
|
* ```
|
||||||
|
* exits: {
|
||||||
|
* somethingHappened: {
|
||||||
|
* responseType: 'forbidden'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* throw 'somethingHappened';
|
||||||
|
* // -or-
|
||||||
|
* throw { somethingHappened: optionalData }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function forbidden(message) {
|
||||||
|
const { res } = this;
|
||||||
|
|
||||||
|
return res.status(403).json({
|
||||||
|
code: 'E_FORBIDDEN',
|
||||||
|
message
|
||||||
|
});
|
||||||
|
};
|
|
@ -15,6 +15,8 @@ module.exports.routes = {
|
||||||
'POST /api/users': 'users/create',
|
'POST /api/users': 'users/create',
|
||||||
'GET /api/users/me': 'users/show',
|
'GET /api/users/me': 'users/show',
|
||||||
'PATCH /api/users/:id': 'users/update',
|
'PATCH /api/users/:id': 'users/update',
|
||||||
|
'PATCH /api/users/:id/email': 'users/update-email',
|
||||||
|
'PATCH /api/users/:id/password': 'users/update-password',
|
||||||
'POST /api/users/:id/upload-avatar': 'users/upload-avatar',
|
'POST /api/users/:id/upload-avatar': 'users/upload-avatar',
|
||||||
'DELETE /api/users/:id': 'users/delete',
|
'DELETE /api/users/:id': 'users/delete',
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue