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 af00e3e191
commit c6ecf126d0
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 = () => ({
type: EntryActionTypes.MODAL_OPEN,
payload: {

View file

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

View file

@ -16,14 +16,7 @@ const Header = React.memo(
isEditable,
onUsers,
onNotificationDelete,
onUserUpdate,
onUserAvatarUpload,
onUserUsernameUpdate,
onUserUsernameUpdateMessageDismiss,
onUserEmailUpdate,
onUserEmailUpdateMessageDismiss,
onUserPasswordUpdate,
onUserPasswordUpdateMessageDismiss,
onUserSettings,
onLogout,
}) => (
<div className={styles.wrapper}>
@ -45,25 +38,7 @@ const Header = React.memo(
)}
</Menu.Item>
</NotificationsPopup>
<UserPopup
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}
>
<UserPopup onSettings={onUserSettings} onLogout={onLogout}>
<Menu.Item className={styles.item}>{user.name}</Menu.Item>
</UserPopup>
</Menu.Menu>
@ -80,14 +55,7 @@ Header.propTypes = {
isEditable: PropTypes.bool.isRequired,
onUsers: PropTypes.func.isRequired,
onNotificationDelete: PropTypes.func.isRequired,
onUserUpdate: 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,
onUserSettings: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
};

View file

@ -10,6 +10,7 @@ const SIZES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large',
MASSIVE: 'massive',
};
// TODO: move to styles
@ -43,6 +44,13 @@ const STYLES = {
padding: '12px 0 10px',
width: '36px',
},
massive: {
fontSize: '36px',
fontWeight: '500',
height: '100px',
padding: '32px 0 10px',
width: '100px',
},
};
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 { 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';
const StepTypes = {
EDIT_NAME: 'EDIT_NAME',
EDIT_USERNAME: 'EDIT_USERNAME',
EDIT_AVATAR: 'EDIT_AVATAR',
EDIT_EMAIL: 'EDIT_EMAIL',
EDIT_PASSWORD: 'EDIT_PASSWORD',
};
const UserStep = React.memo(({ onSettings, onLogout, onClose }) => {
const [t] = useTranslation();
const UserStep = React.memo(
({
email,
name,
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 handleSettingsClick = useCallback(() => {
onSettings();
onClose();
}, [onSettings, onClose]);
const handleNameEditClick = useCallback(() => {
openStep(StepTypes.EDIT_NAME);
}, [openStep]);
const handleAvatarEditClick = useCallback(() => {
openStep(StepTypes.EDIT_AVATAR);
}, [openStep]);
const handleUsernameEditClick = useCallback(() => {
openStep(StepTypes.EDIT_USERNAME);
}, [openStep]);
const handleEmailEditClick = useCallback(() => {
openStep(StepTypes.EDIT_EMAIL);
}, [openStep]);
const handlePasswordEditClick = useCallback(() => {
openStep(StepTypes.EDIT_PASSWORD);
}, [openStep]);
const handleNameUpdate = useCallback(
(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>
</>
);
},
);
return (
<>
<Popup.Header>
{t('common.userActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleSettingsClick}>
{t('common.settings', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={onLogout}>
{t('action.logOut', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
UserStep.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,
onSettings: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
UserStep.defaultProps = {
username: undefined,
avatar: undefined,
};
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;
height: 36px;
overflow: hidden;
margin-left: 8px;
position: relative;
transition: background 0.3s ease;
width: calc(100% - 44px);
width: 100%;
}
.input:hover {

View file

@ -3,12 +3,13 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
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) => {
if (!error) {
@ -35,7 +36,7 @@ const createMessage = (error) => {
};
const EditEmailStep = React.memo(
({ defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onBack, onClose }) => {
({ defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@ -109,7 +110,7 @@ const EditEmailStep = React.memo(
return (
<>
<Popup.Header onBack={onBack}>
<Popup.Header>
{t('common.editEmail', {
context: 'title',
})}
@ -137,19 +138,15 @@ const EditEmailStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
{data.email.trim() !== email && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
<Button
positive
content={t('action.save')}
@ -170,7 +167,6 @@ EditEmailStep.propTypes = {
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,
};
@ -178,4 +174,4 @@ EditEmailStep.defaultProps = {
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 { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
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) => {
if (!error) {
@ -29,7 +30,7 @@ const createMessage = (error) => {
};
const EditPasswordStep = React.memo(
({ defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onBack, onClose }) => {
({ defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@ -83,7 +84,7 @@ const EditPasswordStep = React.memo(
return (
<>
<Popup.Header onBack={onBack}>
<Popup.Header>
{t('common.editPassword', {
context: 'title',
})}
@ -138,7 +139,6 @@ EditPasswordStep.propTypes = {
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,
};
@ -146,4 +146,4 @@ EditPasswordStep.defaultProps = {
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 { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useForm } from '../../hooks';
import { isUsername } from '../../utils/validator';
import { useForm } from '../../../hooks';
import { isUsername } from '../../../utils/validator';
import styles from './EditUsernameStep.module.css';
import styles from './EditUsernamePopup.module.css';
const createMessage = (error) => {
if (!error) {
@ -35,7 +36,7 @@ const createMessage = (error) => {
};
const EditUsernameStep = React.memo(
({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onBack, onClose }) => {
({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@ -109,7 +110,7 @@ const EditUsernameStep = React.memo(
return (
<>
<Popup.Header onBack={onBack}>
<Popup.Header>
{t('common.editUsername', {
context: 'title',
})}
@ -137,19 +138,15 @@ const EditUsernameStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
{data.username.trim() !== (username || '') && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
<Button
positive
content={t('action.save')}
@ -170,7 +167,6 @@ EditUsernameStep.propTypes = {
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,
};
@ -179,4 +175,4 @@ EditUsernameStep.defaultProps = {
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 USER_SETTINGS = 'USER_SETTINGS';
const ADD_PROJECT = 'ADD_PROJECT';
export default {
USERS,
USER_SETTINGS,
ADD_PROJECT,
};

View file

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

View file

@ -3,17 +3,10 @@ import { connect } from 'react-redux';
import { currentUserSelector, notificationsForCurrentUserSelector } from '../selectors';
import {
clearCurrentUserEmailUpdateError,
clearCurrentUserPasswordUpdateError,
clearCurrentUserUsernameUpdateError,
deleteNotification,
logout,
openUserSettingsModal,
openUsersModal,
updateCurrentUser,
updateCurrentUserEmail,
updateCurrentUserPassword,
updateCurrentUserUsername,
uploadCurrentUserAvatar,
} from '../actions/entry';
import Header from '../components/Header';
@ -33,14 +26,7 @@ const mapDispatchToProps = (dispatch) =>
{
onUsers: openUsersModal, // TODO: rename
onNotificationDelete: deleteNotification,
onUserUpdate: updateCurrentUser,
onUserAvatarUpload: uploadCurrentUserAvatar,
onUserUsernameUpdate: updateCurrentUserUsername,
onUserUsernameUpdateMessageDismiss: clearCurrentUserUsernameUpdateError,
onUserEmailUpdate: updateCurrentUserEmail,
onUserEmailUpdateMessageDismiss: clearCurrentUserEmailUpdateError,
onUserPasswordUpdate: updateCurrentUserPassword,
onUserPasswordUpdateMessageDismiss: clearCurrentUserPasswordUpdateError,
onUserSettings: openUserSettingsModal,
onLogout: logout,
},
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: {
common: {
account: 'Account',
actions: 'Actions',
addComment: 'Add comment',
addMember_title: 'Add Member',
@ -27,6 +28,7 @@ export default {
areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?',
areYouSureYouWantToRemoveThisMemberFromProject:
'Are you sure you want to remove this member from project?',
authentication: 'Authentication',
boardNotFound_title: 'Board Not Found',
cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found',
@ -54,7 +56,6 @@ export default {
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',
@ -88,6 +89,7 @@ export default {
'<0>Refresh the page</0> to load last data<br />and receive updates',
removeMember_title: 'Remove Member',
seconds: 'Seconds',
settings: 'Settings',
taskActions_title: 'Task Actions',
tasks: 'Tasks',
time: 'Time',
@ -135,11 +137,9 @@ export default {
deleteTask_title: 'Delete Task',
deleteUser: 'Delete user',
edit: 'Edit',
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',

View file

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

View file

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