Add user settings modal
parent
af00e3e191
commit
c6ecf126d0
@ -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;
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -0,0 +1,3 @@
|
||||
import UserSettingsModal from './UserSettingsModal';
|
||||
|
||||
export default UserSettingsModal;
|
||||
@ -1,8 +1,11 @@
|
||||
const USERS = 'USERS';
|
||||
|
||||
const USER_SETTINGS = 'USER_SETTINGS';
|
||||
|
||||
const ADD_PROJECT = 'ADD_PROJECT';
|
||||
|
||||
export default {
|
||||
USERS,
|
||||
USER_SETTINGS,
|
||||
ADD_PROJECT,
|
||||
};
|
||||
|
||||
@ -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);
|
||||
Loading…
Reference in New Issue