1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 05:09:43 +02:00

Add user settings modal

This commit is contained in:
Maksim Eltyshev 2020-04-08 21:12:58 +05:00
parent ce1e1f741d
commit c4acb3eb24
29 changed files with 570 additions and 455 deletions

View file

@ -8,6 +8,13 @@ export const openUsersModal = () => ({
}, },
}); });
export const openUserSettingsModal = () => ({
type: EntryActionTypes.MODAL_OPEN,
payload: {
type: ModalTypes.USER_SETTINGS,
},
});
export const openAddProjectModal = () => ({ export const openAddProjectModal = () => ({
type: EntryActionTypes.MODAL_OPEN, type: EntryActionTypes.MODAL_OPEN,
payload: { payload: {

View file

@ -4,19 +4,22 @@ import PropTypes from 'prop-types';
import HeaderContainer from '../containers/HeaderContainer'; import HeaderContainer from '../containers/HeaderContainer';
import ProjectsContainer from '../containers/ProjectsContainer'; import ProjectsContainer from '../containers/ProjectsContainer';
import UsersModalContainer from '../containers/UsersModalContainer'; import UsersModalContainer from '../containers/UsersModalContainer';
import UserSettingsModalContainer from '../containers/UserSettingsModalContainer';
import AddProjectModalContainer from '../containers/AddProjectModalContainer'; import AddProjectModalContainer from '../containers/AddProjectModalContainer';
const App = ({ isUsersModalOpened, isAddProjectModalOpened }) => ( const App = ({ isUsersModalOpened, isUserSettingsModalOpened, isAddProjectModalOpened }) => (
<> <>
<HeaderContainer /> <HeaderContainer />
<ProjectsContainer /> <ProjectsContainer />
{isUsersModalOpened && <UsersModalContainer />} {isUsersModalOpened && <UsersModalContainer />}
{isUserSettingsModalOpened && <UserSettingsModalContainer />}
{isAddProjectModalOpened && <AddProjectModalContainer />} {isAddProjectModalOpened && <AddProjectModalContainer />}
</> </>
); );
App.propTypes = { App.propTypes = {
isUsersModalOpened: PropTypes.bool.isRequired, isUsersModalOpened: PropTypes.bool.isRequired,
isUserSettingsModalOpened: PropTypes.bool.isRequired,
isAddProjectModalOpened: PropTypes.bool.isRequired, isAddProjectModalOpened: PropTypes.bool.isRequired,
}; };

View file

@ -16,14 +16,7 @@ const Header = React.memo(
isEditable, isEditable,
onUsers, onUsers,
onNotificationDelete, onNotificationDelete,
onUserUpdate, onUserSettings,
onUserAvatarUpload,
onUserUsernameUpdate,
onUserUsernameUpdateMessageDismiss,
onUserEmailUpdate,
onUserEmailUpdateMessageDismiss,
onUserPasswordUpdate,
onUserPasswordUpdateMessageDismiss,
onLogout, onLogout,
}) => ( }) => (
<div className={styles.wrapper}> <div className={styles.wrapper}>
@ -45,25 +38,7 @@ const Header = React.memo(
)} )}
</Menu.Item> </Menu.Item>
</NotificationsPopup> </NotificationsPopup>
<UserPopup <UserPopup onSettings={onUserSettings} onLogout={onLogout}>
email={user.email}
name={user.name}
username={user.username}
avatar={user.avatar}
isAvatarUploading={user.isAvatarUploading}
usernameUpdateForm={user.usernameUpdateForm}
emailUpdateForm={user.emailUpdateForm}
passwordUpdateForm={user.passwordUpdateForm}
onUpdate={onUserUpdate}
onAvatarUpload={onUserAvatarUpload}
onUsernameUpdate={onUserUsernameUpdate}
onUsernameUpdateMessageDismiss={onUserUsernameUpdateMessageDismiss}
onEmailUpdate={onUserEmailUpdate}
onEmailUpdateMessageDismiss={onUserEmailUpdateMessageDismiss}
onPasswordUpdate={onUserPasswordUpdate}
onPasswordUpdateMessageDismiss={onUserPasswordUpdateMessageDismiss}
onLogout={onLogout}
>
<Menu.Item className={styles.item}>{user.name}</Menu.Item> <Menu.Item className={styles.item}>{user.name}</Menu.Item>
</UserPopup> </UserPopup>
</Menu.Menu> </Menu.Menu>
@ -80,14 +55,7 @@ Header.propTypes = {
isEditable: PropTypes.bool.isRequired, isEditable: PropTypes.bool.isRequired,
onUsers: PropTypes.func.isRequired, onUsers: PropTypes.func.isRequired,
onNotificationDelete: PropTypes.func.isRequired, onNotificationDelete: PropTypes.func.isRequired,
onUserUpdate: PropTypes.func.isRequired, onUserSettings: PropTypes.func.isRequired,
onUserAvatarUpload: PropTypes.func.isRequired,
onUserUsernameUpdate: PropTypes.func.isRequired,
onUserUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onUserEmailUpdate: PropTypes.func.isRequired,
onUserEmailUpdateMessageDismiss: PropTypes.func.isRequired,
onUserPasswordUpdate: PropTypes.func.isRequired,
onUserPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired, onLogout: PropTypes.func.isRequired,
}; };

View file

@ -10,6 +10,7 @@ const SIZES = {
SMALL: 'small', SMALL: 'small',
MEDIUM: 'medium', MEDIUM: 'medium',
LARGE: 'large', LARGE: 'large',
MASSIVE: 'massive',
}; };
// TODO: move to styles // TODO: move to styles
@ -43,6 +44,13 @@ const STYLES = {
padding: '12px 0 10px', padding: '12px 0 10px',
width: '36px', width: '36px',
}, },
massive: {
fontSize: '36px',
fontWeight: '500',
height: '100px',
padding: '32px 0 10px',
width: '100px',
},
}; };
const COLORS = [ const COLORS = [

View file

@ -1,72 +0,0 @@
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 User from '../User';
import styles from './EditAvatarStep.module.css';
const EditAvatarStep = React.memo(
({ defaultValue, name, isUploading, onUpload, onClear, onBack }) => {
const [t] = useTranslation();
const field = useRef(null);
const handleFieldChange = useCallback(
({ target }) => {
if (target.files[0]) {
onUpload(target.files[0]);
target.value = null; // eslint-disable-line no-param-reassign
}
},
[onUpload],
);
useEffect(() => {
field.current.focus();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editAvatar', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<User name={name} avatar={defaultValue} size="large" />
<div className={styles.input}>
<Button content={t('action.uploadNewAvatar')} className={styles.customButton} />
<input
ref={field}
type="file"
accept="image/*"
disabled={isUploading}
className={styles.file}
onChange={handleFieldChange}
/>
</div>
{defaultValue && <Button negative content={t('action.deleteAvatar')} onClick={onClear} />}
</Popup.Content>
</>
);
},
);
EditAvatarStep.propTypes = {
defaultValue: PropTypes.string,
name: PropTypes.string.isRequired,
isUploading: PropTypes.bool.isRequired,
onUpload: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
EditAvatarStep.defaultProps = {
defaultValue: undefined,
};
export default EditAvatarStep;

View file

@ -1,67 +0,0 @@
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 { useField } from '../../hooks';
import styles from './EditNameStep.module.css';
const EditNameStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) => {
const [t] = useTranslation();
const [value, handleFieldChange] = useField(defaultValue);
const field = useRef(null);
const handleSubmit = useCallback(() => {
const cleanValue = value.trim();
if (!cleanValue) {
field.current.select();
return;
}
if (cleanValue !== defaultValue) {
onUpdate(cleanValue);
}
onClose();
}, [defaultValue, onUpdate, onClose, value]);
useEffect(() => {
field.current.select();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editName', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.name')}</div>
<Input
fluid
ref={field}
value={value}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.save')} />
</Form>
</Popup.Content>
</>
);
});
EditNameStep.propTypes = {
defaultValue: PropTypes.string.isRequired,
onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default EditNameStep;

View file

@ -5,218 +5,45 @@ 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 EditNameStep from './EditNameStep';
import EditUsernameStep from './EditUsernameStep';
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 UserStep = React.memo(({ onSettings, onLogout, onClose }) => {
EDIT_NAME: 'EDIT_NAME', const [t] = useTranslation();
EDIT_USERNAME: 'EDIT_USERNAME',
EDIT_AVATAR: 'EDIT_AVATAR',
EDIT_EMAIL: 'EDIT_EMAIL',
EDIT_PASSWORD: 'EDIT_PASSWORD',
};
const UserStep = React.memo( const handleSettingsClick = useCallback(() => {
({ onSettings();
email, onClose();
name, }, [onSettings, onClose]);
username,
avatar,
isAvatarUploading,
usernameUpdateForm,
emailUpdateForm,
passwordUpdateForm,
onUpdate,
onAvatarUpload,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onLogout,
onClose,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleNameEditClick = useCallback(() => { return (
openStep(StepTypes.EDIT_NAME); <>
}, [openStep]); <Popup.Header>
{t('common.userActions', {
const handleAvatarEditClick = useCallback(() => { context: 'title',
openStep(StepTypes.EDIT_AVATAR); })}
}, [openStep]); </Popup.Header>
<Popup.Content>
const handleUsernameEditClick = useCallback(() => { <Menu secondary vertical className={styles.menu}>
openStep(StepTypes.EDIT_USERNAME); <Menu.Item className={styles.menuItem} onClick={handleSettingsClick}>
}, [openStep]); {t('common.settings', {
context: 'title',
const handleEmailEditClick = useCallback(() => { })}
openStep(StepTypes.EDIT_EMAIL); </Menu.Item>
}, [openStep]); <Menu.Item className={styles.menuItem} onClick={onLogout}>
{t('action.logOut', {
const handlePasswordEditClick = useCallback(() => { context: 'title',
openStep(StepTypes.EDIT_PASSWORD); })}
}, [openStep]); </Menu.Item>
</Menu>
const handleNameUpdate = useCallback( </Popup.Content>
(newName) => { </>
onUpdate({ );
name: newName, });
});
},
[onUpdate],
);
const handleAvatarClear = useCallback(() => {
onUpdate({
avatar: null,
});
}, [onUpdate]);
if (step) {
switch (step.type) {
case StepTypes.EDIT_NAME:
return (
<EditNameStep
defaultValue={name}
onUpdate={handleNameUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_AVATAR:
return (
<EditAvatarStep
defaultValue={avatar}
name={name}
isUploading={isAvatarUploading}
onUpload={onAvatarUpload}
onClear={handleAvatarClear}
onBack={handleBack}
/>
);
case StepTypes.EDIT_USERNAME:
return (
<EditUsernameStep
defaultData={usernameUpdateForm.data}
username={username}
isSubmitting={usernameUpdateForm.isSubmitting}
error={usernameUpdateForm.error}
onUpdate={onUsernameUpdate}
onMessageDismiss={onUsernameUpdateMessageDismiss}
onBack={handleBack}
onClose={onClose}
/>
);
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:
}
}
return (
<>
<Popup.Header>
{t('common.userActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleNameEditClick}>
{t('action.editName', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleAvatarEditClick}>
{t('action.editAvatar', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleUsernameEditClick}>
{t('action.editUsername', {
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',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
UserStep.propTypes = { UserStep.propTypes = {
email: PropTypes.string.isRequired, onSettings: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
username: PropTypes.string,
avatar: PropTypes.string,
isAvatarUploading: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired,
emailUpdateForm: PropTypes.object.isRequired,
passwordUpdateForm: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired,
onAvatarUpload: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: 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,
}; };
UserStep.defaultProps = {
username: undefined,
avatar: undefined,
};
export default withPopup(UserStep); export default withPopup(UserStep);

View file

@ -0,0 +1,144 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Header, Tab } from 'semantic-ui-react';
import EditInformation from './EditInformation';
import EditAvatarPopup from './EditAvatarPopup';
import EditUsernamePopup from './EditUsernamePopup';
import EditEmailPopup from './EditEmailPopup';
import EditPasswordPopup from './EditPasswordPopup';
import User from '../../User';
import styles from './AccountPane.module.css';
const AccountPane = React.memo(
({
email,
name,
username,
avatar,
isAvatarUploading,
usernameUpdateForm,
emailUpdateForm,
passwordUpdateForm,
onUpdate,
onAvatarUpload,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
}) => {
const [t] = useTranslation();
const handleAvatarDelete = useCallback(() => {
onUpdate({
avatar: null,
});
}, [onUpdate]);
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<EditAvatarPopup
defaultValue={avatar}
onUpload={onAvatarUpload}
onDelete={handleAvatarDelete}
>
<User name={name} avatar={avatar} size="massive" isDisabled={isAvatarUploading} />
</EditAvatarPopup>
<br />
<br />
<EditInformation
defaultData={{
name,
}}
onUpdate={onUpdate}
/>
<Divider horizontal section>
<Header as="h4">
{t('common.authentication', {
context: 'title',
})}
</Header>
</Divider>
<div className={styles.action}>
<EditUsernamePopup
defaultData={usernameUpdateForm.data}
username={username}
isSubmitting={usernameUpdateForm.isSubmitting}
error={usernameUpdateForm.error}
onUpdate={onUsernameUpdate}
onMessageDismiss={onUsernameUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editUsername', {
context: 'title',
})}
</Button>
</EditUsernamePopup>
</div>
<div className={styles.action}>
<EditEmailPopup
defaultData={emailUpdateForm.data}
email={email}
isSubmitting={emailUpdateForm.isSubmitting}
error={emailUpdateForm.error}
onUpdate={onEmailUpdate}
onMessageDismiss={onEmailUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editEmail', {
context: 'title',
})}
</Button>
</EditEmailPopup>
</div>
<div className={styles.action}>
<EditPasswordPopup
defaultData={passwordUpdateForm.data}
isSubmitting={passwordUpdateForm.isSubmitting}
error={passwordUpdateForm.error}
onUpdate={onPasswordUpdate}
onMessageDismiss={onPasswordUpdateMessageDismiss}
>
<Button className={styles.actionButton}>
{t('action.editPassword', {
context: 'title',
})}
</Button>
</EditPasswordPopup>
</div>
</Tab.Pane>
);
},
);
AccountPane.propTypes = {
email: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
username: PropTypes.string,
avatar: PropTypes.string,
isAvatarUploading: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired,
emailUpdateForm: PropTypes.object.isRequired,
passwordUpdateForm: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired,
onAvatarUpload: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onEmailUpdate: PropTypes.func.isRequired,
onEmailUpdateMessageDismiss: PropTypes.func.isRequired,
onPasswordUpdate: PropTypes.func.isRequired,
onPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
};
AccountPane.defaultProps = {
username: undefined,
avatar: undefined,
};
export default AccountPane;

View file

@ -0,0 +1,30 @@
.action {
border: none;
display: inline-block;
height: 36px;
overflow: hidden;
position: relative;
transition: background 0.3s ease;
width: 100%;
}
.action:hover {
background: #e9e9e9 !important;
}
.actionButton {
background: transparent !important;
color: #6b808c !important;
font-weight: normal !important;
height: 36px;
line-height: 24px !important;
padding: 6px 12px !important;
text-align: left !important;
text-decoration: underline !important;
width: 100%;
}
.wrapper {
border: none !important;
box-shadow: none !important;
}

View file

@ -0,0 +1,71 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { withPopup } from '../../../lib/popup';
import { Popup } from '../../../lib/custom-ui';
import styles from './EditAvatarPopup.module.css';
const EditAvatarStep = React.memo(({ defaultValue, onUpload, onDelete, onClose }) => {
const [t] = useTranslation();
const field = useRef(null);
const handleFieldChange = useCallback(
({ target }) => {
if (target.files[0]) {
onUpload(target.files[0]);
onClose();
}
},
[onUpload, onClose],
);
const handleDeleteClick = useCallback(() => {
onDelete();
onClose();
}, [onDelete, onClose]);
useEffect(() => {
field.current.focus();
}, []);
return (
<>
<Popup.Header>
{t('common.editAvatar', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<div className={styles.input}>
<Button content={t('action.uploadNewAvatar')} className={styles.customButton} />
<input
ref={field}
type="file"
accept="image/*"
className={styles.file}
onChange={handleFieldChange}
/>
</div>
{defaultValue && (
<Button negative content={t('action.deleteAvatar')} onClick={handleDeleteClick} />
)}
</Popup.Content>
</>
);
});
EditAvatarStep.propTypes = {
defaultValue: PropTypes.string,
onUpload: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
EditAvatarStep.defaultProps = {
defaultValue: undefined,
};
export default withPopup(EditAvatarStep);

View file

@ -24,10 +24,9 @@
display: inline-block; display: inline-block;
height: 36px; height: 36px;
overflow: hidden; overflow: hidden;
margin-left: 8px;
position: relative; position: relative;
transition: background 0.3s ease; transition: background 0.3s ease;
width: calc(100% - 44px); width: 100%;
} }
.input:hover { .input:hover {

View file

@ -3,12 +3,13 @@ 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 { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks'; import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui'; import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm } from '../../hooks'; import { useForm } from '../../../hooks';
import styles from './EditNameStep.module.css'; import styles from './EditEmailPopup.module.css';
const createMessage = (error) => { const createMessage = (error) => {
if (!error) { if (!error) {
@ -35,7 +36,7 @@ const createMessage = (error) => {
}; };
const EditEmailStep = React.memo( const EditEmailStep = React.memo(
({ defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onBack, onClose }) => { ({ defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting); const wasSubmitting = usePrevious(isSubmitting);
@ -109,7 +110,7 @@ const EditEmailStep = React.memo(
return ( return (
<> <>
<Popup.Header onBack={onBack}> <Popup.Header>
{t('common.editEmail', { {t('common.editEmail', {
context: 'title', context: 'title',
})} })}
@ -137,19 +138,15 @@ const EditEmailStep = React.memo(
className={styles.field} className={styles.field}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
{data.email.trim() !== email && ( <div className={styles.text}>{t('common.currentPassword')}</div>
<> <Input.Password
<div className={styles.text}>{t('common.currentPassword')}</div> fluid
<Input.Password ref={currentPasswordField}
fluid name="currentPassword"
ref={currentPasswordField} value={data.currentPassword}
name="currentPassword" className={styles.field}
value={data.currentPassword} onChange={handleFieldChange}
className={styles.field} />
onChange={handleFieldChange}
/>
</>
)}
<Button <Button
positive positive
content={t('action.save')} content={t('action.save')}
@ -170,7 +167,6 @@ EditEmailStep.propTypes = {
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired, onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
@ -178,4 +174,4 @@ EditEmailStep.defaultProps = {
error: undefined, error: undefined,
}; };
export default EditEmailStep; export default withPopup(EditEmailStep);

View file

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

View file

@ -2,12 +2,13 @@ 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 { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks'; import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui'; import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm } from '../../hooks'; import { useForm } from '../../../hooks';
import styles from './EditNameStep.module.css'; import styles from './EditPasswordPopup.module.css';
const createMessage = (error) => { const createMessage = (error) => {
if (!error) { if (!error) {
@ -29,7 +30,7 @@ const createMessage = (error) => {
}; };
const EditPasswordStep = React.memo( const EditPasswordStep = React.memo(
({ defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onBack, onClose }) => { ({ defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting); const wasSubmitting = usePrevious(isSubmitting);
@ -83,7 +84,7 @@ const EditPasswordStep = React.memo(
return ( return (
<> <>
<Popup.Header onBack={onBack}> <Popup.Header>
{t('common.editPassword', { {t('common.editPassword', {
context: 'title', context: 'title',
})} })}
@ -138,7 +139,6 @@ EditPasswordStep.propTypes = {
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired, onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
@ -146,4 +146,4 @@ EditPasswordStep.defaultProps = {
error: undefined, error: undefined,
}; };
export default EditPasswordStep; export default withPopup(EditPasswordStep);

View file

@ -2,13 +2,14 @@ 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 { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks'; import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui'; import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm } from '../../hooks'; import { useForm } from '../../../hooks';
import { isUsername } from '../../utils/validator'; import { isUsername } from '../../../utils/validator';
import styles from './EditUsernameStep.module.css'; import styles from './EditUsernamePopup.module.css';
const createMessage = (error) => { const createMessage = (error) => {
if (!error) { if (!error) {
@ -35,7 +36,7 @@ const createMessage = (error) => {
}; };
const EditUsernameStep = React.memo( const EditUsernameStep = React.memo(
({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onBack, onClose }) => { ({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting); const wasSubmitting = usePrevious(isSubmitting);
@ -109,7 +110,7 @@ const EditUsernameStep = React.memo(
return ( return (
<> <>
<Popup.Header onBack={onBack}> <Popup.Header>
{t('common.editUsername', { {t('common.editUsername', {
context: 'title', context: 'title',
})} })}
@ -137,19 +138,15 @@ const EditUsernameStep = React.memo(
className={styles.field} className={styles.field}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
{data.username.trim() !== (username || '') && ( <div className={styles.text}>{t('common.currentPassword')}</div>
<> <Input.Password
<div className={styles.text}>{t('common.currentPassword')}</div> fluid
<Input.Password ref={currentPasswordField}
fluid name="currentPassword"
ref={currentPasswordField} value={data.currentPassword}
name="currentPassword" className={styles.field}
value={data.currentPassword} onChange={handleFieldChange}
className={styles.field} />
onChange={handleFieldChange}
/>
</>
)}
<Button <Button
positive positive
content={t('action.save')} content={t('action.save')}
@ -170,7 +167,6 @@ EditUsernameStep.propTypes = {
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired, onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
@ -179,4 +175,4 @@ EditUsernameStep.defaultProps = {
error: undefined, error: undefined,
}; };
export default EditUsernameStep; export default withPopup(EditUsernameStep);

View file

@ -0,0 +1,3 @@
import AccountPane from './AccountPane';
export default AccountPane;

View file

@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Modal, Tab } from 'semantic-ui-react';
import AccountPane from './AccountPane';
const UserSettingsModal = React.memo(
({
email,
name,
username,
avatar,
isAvatarUploading,
usernameUpdateForm,
emailUpdateForm,
passwordUpdateForm,
onUpdate,
onAvatarUpload,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onClose,
}) => {
const [t] = useTranslation();
const panes = [
{
menuItem: t('common.account', {
context: 'title',
}),
render: () => (
<AccountPane
email={email}
name={name}
username={username}
avatar={avatar}
isAvatarUploading={isAvatarUploading}
usernameUpdateForm={usernameUpdateForm}
emailUpdateForm={emailUpdateForm}
passwordUpdateForm={passwordUpdateForm}
onUpdate={onUpdate}
onAvatarUpload={onAvatarUpload}
onUsernameUpdate={onUsernameUpdate}
onUsernameUpdateMessageDismiss={onUsernameUpdateMessageDismiss}
onEmailUpdate={onEmailUpdate}
onEmailUpdateMessageDismiss={onEmailUpdateMessageDismiss}
onPasswordUpdate={onPasswordUpdate}
onPasswordUpdateMessageDismiss={onPasswordUpdateMessageDismiss}
/>
),
},
];
return (
<Modal open closeIcon size="small" centered={false} onClose={onClose}>
<Modal.Content>
<Tab menu={{ secondary: true, pointing: true }} panes={panes} />
</Modal.Content>
</Modal>
);
},
);
UserSettingsModal.propTypes = {
email: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
username: PropTypes.string,
avatar: PropTypes.string,
isAvatarUploading: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired,
emailUpdateForm: PropTypes.object.isRequired,
passwordUpdateForm: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired,
onAvatarUpload: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onEmailUpdate: PropTypes.func.isRequired,
onEmailUpdateMessageDismiss: PropTypes.func.isRequired,
onPasswordUpdate: PropTypes.func.isRequired,
onPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
UserSettingsModal.defaultProps = {
username: undefined,
avatar: undefined,
};
export default UserSettingsModal;

View file

@ -0,0 +1,3 @@
import UserSettingsModal from './UserSettingsModal';
export default UserSettingsModal;

View file

@ -1,8 +1,11 @@
const USERS = 'USERS'; const USERS = 'USERS';
const USER_SETTINGS = 'USER_SETTINGS';
const ADD_PROJECT = 'ADD_PROJECT'; const ADD_PROJECT = 'ADD_PROJECT';
export default { export default {
USERS, USERS,
USER_SETTINGS,
ADD_PROJECT, ADD_PROJECT,
}; };

View file

@ -9,6 +9,7 @@ const mapStateToProps = (state) => {
return { return {
isUsersModalOpened: currentModal === ModalTypes.USERS, isUsersModalOpened: currentModal === ModalTypes.USERS,
isUserSettingsModalOpened: currentModal === ModalTypes.USER_SETTINGS,
isAddProjectModalOpened: currentModal === ModalTypes.ADD_PROJECT, isAddProjectModalOpened: currentModal === ModalTypes.ADD_PROJECT,
}; };
}; };

View file

@ -3,17 +3,10 @@ import { connect } from 'react-redux';
import { currentUserSelector, notificationsForCurrentUserSelector } from '../selectors'; import { currentUserSelector, notificationsForCurrentUserSelector } from '../selectors';
import { import {
clearCurrentUserEmailUpdateError,
clearCurrentUserPasswordUpdateError,
clearCurrentUserUsernameUpdateError,
deleteNotification, deleteNotification,
logout, logout,
openUserSettingsModal,
openUsersModal, openUsersModal,
updateCurrentUser,
updateCurrentUserEmail,
updateCurrentUserPassword,
updateCurrentUserUsername,
uploadCurrentUserAvatar,
} from '../actions/entry'; } from '../actions/entry';
import Header from '../components/Header'; import Header from '../components/Header';
@ -33,14 +26,7 @@ const mapDispatchToProps = (dispatch) =>
{ {
onUsers: openUsersModal, // TODO: rename onUsers: openUsersModal, // TODO: rename
onNotificationDelete: deleteNotification, onNotificationDelete: deleteNotification,
onUserUpdate: updateCurrentUser, onUserSettings: openUserSettingsModal,
onUserAvatarUpload: uploadCurrentUserAvatar,
onUserUsernameUpdate: updateCurrentUserUsername,
onUserUsernameUpdateMessageDismiss: clearCurrentUserUsernameUpdateError,
onUserEmailUpdate: updateCurrentUserEmail,
onUserEmailUpdateMessageDismiss: clearCurrentUserEmailUpdateError,
onUserPasswordUpdate: updateCurrentUserPassword,
onUserPasswordUpdateMessageDismiss: clearCurrentUserPasswordUpdateError,
onLogout: logout, onLogout: logout,
}, },
dispatch, dispatch,

View file

@ -0,0 +1,58 @@
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { currentUserSelector } from '../selectors';
import {
clearCurrentUserEmailUpdateError,
clearCurrentUserPasswordUpdateError,
clearCurrentUserUsernameUpdateError,
closeModal,
updateCurrentUser,
updateCurrentUserEmail,
updateCurrentUserPassword,
updateCurrentUserUsername,
uploadCurrentUserAvatar,
} from '../actions/entry';
import UserSettingsModal from '../components/UserSettingsModal';
const mapStateToProps = (state) => {
const {
email,
name,
username,
avatar,
isAvatarUploading,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
} = currentUserSelector(state);
return {
email,
name,
username,
avatar,
isAvatarUploading,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
};
};
const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
onUpdate: updateCurrentUser,
onAvatarUpload: uploadCurrentUserAvatar,
onUsernameUpdate: updateCurrentUserUsername,
onUsernameUpdateMessageDismiss: clearCurrentUserUsernameUpdateError,
onEmailUpdate: updateCurrentUserEmail,
onEmailUpdateMessageDismiss: clearCurrentUserEmailUpdateError,
onPasswordUpdate: updateCurrentUserPassword,
onPasswordUpdateMessageDismiss: clearCurrentUserPasswordUpdateError,
onClose: closeModal,
},
dispatch,
);
export default connect(mapStateToProps, mapDispatchToProps)(UserSettingsModal);

View file

@ -9,6 +9,7 @@ export default {
translation: { translation: {
common: { common: {
account: 'Account',
actions: 'Actions', actions: 'Actions',
addComment: 'Add comment', addComment: 'Add comment',
addMember_title: 'Add Member', addMember_title: 'Add Member',
@ -27,6 +28,7 @@ export default {
areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?', areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?',
areYouSureYouWantToRemoveThisMemberFromProject: areYouSureYouWantToRemoveThisMemberFromProject:
'Are you sure you want to remove this member from project?', 'Are you sure you want to remove this member from project?',
authentication: 'Authentication',
boardNotFound_title: 'Board Not Found', boardNotFound_title: 'Board Not Found',
cardActions_title: 'Card Actions', cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found', cardNotFound_title: 'Card Not Found',
@ -54,7 +56,6 @@ export default {
editDueDate_title: 'Edit Due Date', editDueDate_title: 'Edit Due Date',
editEmail_title: 'Edit E-mail', editEmail_title: 'Edit E-mail',
editLabel_title: 'Edit Label', editLabel_title: 'Edit Label',
editName_title: 'Edit Name',
editPassword_title: 'Edit Password', editPassword_title: 'Edit Password',
editProject_title: 'Edit Project', editProject_title: 'Edit Project',
editTimer_title: 'Edit Timer', editTimer_title: 'Edit Timer',
@ -88,6 +89,7 @@ export default {
'<0>Refresh the page</0> to load last data<br />and receive updates', '<0>Refresh the page</0> to load last data<br />and receive updates',
removeMember_title: 'Remove Member', removeMember_title: 'Remove Member',
seconds: 'Seconds', seconds: 'Seconds',
settings: 'Settings',
taskActions_title: 'Task Actions', taskActions_title: 'Task Actions',
tasks: 'Tasks', tasks: 'Tasks',
time: 'Time', time: 'Time',
@ -135,11 +137,9 @@ export default {
deleteTask_title: 'Delete Task', deleteTask_title: 'Delete Task',
deleteUser: 'Delete user', deleteUser: 'Delete user',
edit: 'Edit', edit: 'Edit',
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', editEmail_title: 'Edit E-mail',
editName_title: 'Edit Name',
editPassword_title: 'Edit Password', editPassword_title: 'Edit Password',
editTask_title: 'Edit Task', editTask_title: 'Edit Task',
editTimer_title: 'Edit Timer', editTimer_title: 'Edit Timer',

View file

@ -13,6 +13,7 @@ export default {
translation: { translation: {
common: { common: {
account: 'Учетная запись',
actions: 'Действия', actions: 'Действия',
addComment: 'Добавление комментария', addComment: 'Добавление комментария',
addMember: 'Добавление участника', addMember: 'Добавление участника',
@ -31,6 +32,7 @@ export default {
areYouSureYouWantToDeleteThisUser: 'Вы уверены, что хотите удалить этого пользователя?', areYouSureYouWantToDeleteThisUser: 'Вы уверены, что хотите удалить этого пользователя?',
areYouSureYouWantToRemoveThisMemberFromProject: areYouSureYouWantToRemoveThisMemberFromProject:
'Вы уверены, что хотите удалить этого участника из проекта?', 'Вы уверены, что хотите удалить этого участника из проекта?',
authentication: 'Аутентификация',
boardNotFound: 'Доска не найдена', boardNotFound: 'Доска не найдена',
cardActions: 'Действия с карточкой', cardActions: 'Действия с карточкой',
cardNotFound: 'Карточка не найдена', cardNotFound: 'Карточка не найдена',
@ -58,7 +60,6 @@ export default {
editDueDate: 'Изменение срока', editDueDate: 'Изменение срока',
editEmail: 'Изменение e-mail', editEmail: 'Изменение e-mail',
editLabel: 'Изменения метки', editLabel: 'Изменения метки',
editName: 'Изменение имени',
editPassword: 'Изменение пароля', editPassword: 'Изменение пароля',
editProject: 'Изменение проекта', editProject: 'Изменение проекта',
editTimer: 'Изменение таймера', editTimer: 'Изменение таймера',
@ -92,6 +93,7 @@ export default {
'<0>Обновите страницу</0>, чтобы загрузить<br />актуальные данные и получать обновления', '<0>Обновите страницу</0>, чтобы загрузить<br />актуальные данные и получать обновления',
removeMember: 'Удаление участника', removeMember: 'Удаление участника',
seconds: 'Секунды', seconds: 'Секунды',
settings: 'Настройки',
taskActions: 'Действия с задачей', taskActions: 'Действия с задачей',
tasks: 'Задачи', tasks: 'Задачи',
time: 'Время', time: 'Время',
@ -136,11 +138,9 @@ export default {
deleteTask: 'Удалить задачу', deleteTask: 'Удалить задачу',
deleteUser: 'Удалить пользователя', deleteUser: 'Удалить пользователя',
edit: 'Изменить', edit: 'Изменить',
editAvatar: 'Изменить аватар',
editDueDate: 'Изменить срок', editDueDate: 'Изменить срок',
editDescription: 'Изменить описание', editDescription: 'Изменить описание',
editEmail: 'Изменить e-mail', editEmail: 'Изменить e-mail',
editName: 'Изменить имя',
editPassword: 'Изменить пароль', editPassword: 'Изменить пароль',
editTask: 'Изменить задачу', editTask: 'Изменить задачу',
editTimer: 'Изменить таймер', editTimer: 'Изменить таймер',

View file

@ -32,7 +32,7 @@ const createReceiver = () => {
} }
firstFileHandled = true; firstFileHandled = true;
const resize = sharp().resize(36, 36).jpeg(); const resize = sharp().resize(100, 100).jpeg();
const transform = new stream.Transform({ const transform = new stream.Transform({
transform(chunk, streamEncoding, callback) { transform(chunk, streamEncoding, callback) {