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