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:
parent
ce1e1f741d
commit
c4acb3eb24
29 changed files with 570 additions and 455 deletions
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
|
@ -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 {
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,3 @@
|
||||||
|
import AccountPane from './AccountPane';
|
||||||
|
|
||||||
|
export default AccountPane;
|
|
@ -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;
|
3
client/src/components/UserSettingsModal/index.js
Normal file
3
client/src/components/UserSettingsModal/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import UserSettingsModal from './UserSettingsModal';
|
||||||
|
|
||||||
|
export default UserSettingsModal;
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
58
client/src/containers/UserSettingsModalContainer.js
Normal file
58
client/src/containers/UserSettingsModalContainer.js
Normal 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);
|
|
@ -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',
|
||||||
|
|
|
@ -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: 'Изменить таймер',
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue