1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Add ability to edit users

Closes #60
This commit is contained in:
Maksim Eltyshev 2022-06-15 14:13:22 +02:00
parent b810dcced7
commit dd83278c83
30 changed files with 775 additions and 204 deletions

View file

@ -41,6 +41,14 @@ export const handleUserUpdate = (user) => ({
},
});
export const updateUserEmail = (id, data) => ({
type: EntryActionTypes.USER_EMAIL_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentUserEmail = (data) => ({
type: EntryActionTypes.CURRENT_USER_EMAIL_UPDATE,
payload: {
@ -48,11 +56,26 @@ export const updateCurrentUserEmail = (data) => ({
},
});
export const clearUserEmailUpdateError = (id) => ({
type: EntryActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
export const clearCurrentUserEmailUpdateError = () => ({
type: EntryActionTypes.CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR,
payload: {},
});
export const updateUserPassword = (id, data) => ({
type: EntryActionTypes.USER_PASSWORD_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentUserPassword = (data) => ({
type: EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE,
payload: {
@ -60,11 +83,26 @@ export const updateCurrentUserPassword = (data) => ({
},
});
export const clearUserPasswordUpdateError = (id) => ({
type: EntryActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
export const clearCurrentUserPasswordUpdateError = () => ({
type: EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR,
payload: {},
});
export const updateUserUsername = (id, data) => ({
type: EntryActionTypes.USER_USERNAME_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentUserUsername = (data) => ({
type: EntryActionTypes.CURRENT_USER_USERNAME_UPDATE,
payload: {
@ -72,6 +110,13 @@ export const updateCurrentUserUsername = (data) => ({
},
});
export const clearUserUsernameUpdateError = (id) => ({
type: EntryActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
export const clearCurrentUserUsernameUpdateError = () => ({
type: EntryActionTypes.CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR,
payload: {},

View file

@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import UserEmailEditStep from './UserEmailEditStep';
export default withPopup(UserEmailEditStep);

View file

@ -1,15 +1,15 @@
import omit from 'lodash/omit';
import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm } from '../../../hooks';
import { useForm } from '../../hooks';
import styles from './EmailEditPopup.module.scss';
import styles from './UserEmailEditStep.module.scss';
const createMessage = (error) => {
if (!error) {
@ -35,8 +35,18 @@ const createMessage = (error) => {
}
};
const EmailEditStep = React.memo(
({ defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const UserEmailEditStep = React.memo(
({
defaultData,
email,
isSubmitting,
error,
usePasswordConfirmation,
onUpdate,
onMessageDismiss,
onBack,
onClose,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@ -68,13 +78,13 @@ const EmailEditStep = React.memo(
return;
}
if (!cleanData.currentPassword) {
if (usePasswordConfirmation && !cleanData.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(cleanData);
}, [email, onUpdate, onClose, data]);
onUpdate(usePasswordConfirmation ? cleanData : omit(cleanData, 'currentPassword'));
}, [email, usePasswordConfirmation, onUpdate, onClose, data]);
useEffect(() => {
emailField.current.select();
@ -110,7 +120,7 @@ const EmailEditStep = React.memo(
return (
<>
<Popup.Header>
<Popup.Header onBack={onBack}>
{t('common.editEmail', {
context: 'title',
})}
@ -138,6 +148,8 @@ const EmailEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
{usePasswordConfirmation && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
@ -147,6 +159,8 @@ const EmailEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<Button
positive
content={t('action.save')}
@ -160,18 +174,22 @@ const EmailEditStep = React.memo(
},
);
EmailEditStep.propTypes = {
UserEmailEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
email: PropTypes.string.isRequired,
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
usePasswordConfirmation: PropTypes.bool,
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
EmailEditStep.defaultProps = {
UserEmailEditStep.defaultProps = {
error: undefined,
usePasswordConfirmation: false,
onBack: undefined,
};
export default withPopup(EmailEditStep);
export default UserEmailEditStep;

View file

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

View file

@ -5,11 +5,11 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
import { useForm } from '../../../hooks';
import { useForm } from '../../hooks';
import styles from './InformationEdit.module.scss';
import styles from './UserInformationEdit.module.scss';
const InformationEdit = React.memo(({ defaultData, onUpdate }) => {
const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
@ -72,9 +72,9 @@ const InformationEdit = React.memo(({ defaultData, onUpdate }) => {
);
});
InformationEdit.propTypes = {
UserInformationEdit.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
};
export default InformationEdit;
export default UserInformationEdit;

View file

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

View file

@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Popup } from '../lib/custom-ui';
import UserInformationEdit from './UserInformationEdit';
const UserInformationEditStep = React.memo(({ defaultData, onUpdate, onBack, onClose }) => {
const [t] = useTranslation();
const handleUpdate = useCallback(
(data) => {
onUpdate(data);
onClose();
},
[onUpdate, onClose],
);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editInformation', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<UserInformationEdit defaultData={defaultData} onUpdate={handleUpdate} />
</Popup.Content>
</>
);
});
UserInformationEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
UserInformationEditStep.defaultProps = {
onBack: undefined,
};
export default UserInformationEditStep;

View file

@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import UserPasswordEditStep from './UserPasswordEditStep';
export default withPopup(UserPasswordEditStep);

View file

@ -1,14 +1,14 @@
import omit from 'lodash/omit';
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 { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm } from '../../../hooks';
import { useForm } from '../../hooks';
import styles from './PasswordEditPopup.module.scss';
import styles from './UserPasswordEditStep.module.scss';
const createMessage = (error) => {
if (!error) {
@ -29,8 +29,17 @@ const createMessage = (error) => {
}
};
const PasswordEditStep = React.memo(
({ defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const UserPasswordEditStep = React.memo(
({
defaultData,
isSubmitting,
error,
usePasswordConfirmation,
onUpdate,
onMessageDismiss,
onBack,
onClose,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@ -52,13 +61,13 @@ const PasswordEditStep = React.memo(
return;
}
if (!data.currentPassword) {
if (usePasswordConfirmation && !data.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(data);
}, [onUpdate, data]);
onUpdate(usePasswordConfirmation ? data : omit(data, 'currentPassword'));
}, [usePasswordConfirmation, onUpdate, data]);
useEffect(() => {
passwordField.current.select();
@ -84,7 +93,7 @@ const PasswordEditStep = React.memo(
return (
<>
<Popup.Header>
<Popup.Header onBack={onBack}>
{t('common.editPassword', {
context: 'title',
})}
@ -111,6 +120,8 @@ const PasswordEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
{usePasswordConfirmation && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
@ -120,6 +131,8 @@ const PasswordEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<Button
positive
content={t('action.save')}
@ -133,17 +146,21 @@ const PasswordEditStep = React.memo(
},
);
PasswordEditStep.propTypes = {
UserPasswordEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
usePasswordConfirmation: PropTypes.bool,
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
PasswordEditStep.defaultProps = {
UserPasswordEditStep.defaultProps = {
error: undefined,
usePasswordConfirmation: false,
onBack: undefined,
};
export default withPopup(PasswordEditStep);
export default UserPasswordEditStep;

View file

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

View file

@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Header, Tab } from 'semantic-ui-react';
import InformationEdit from './InformationEdit';
import AvatarEditPopup from './AvatarEditPopup';
import UsernameEditPopup from './UsernameEditPopup';
import EmailEditPopup from './EmailEditPopup';
import PasswordEditPopup from './PasswordEditPopup';
import User from '../../User';
import UserInformationEdit from '../../UserInformationEdit';
import UserUsernameEditPopup from '../../UserUsernameEditPopup';
import UserEmailEditPopup from '../../UserEmailEditPopup';
import UserPasswordEditPopup from '../../UserPasswordEditPopup';
import styles from './AccountPane.module.scss';
@ -52,7 +52,7 @@ const AccountPane = React.memo(
</AvatarEditPopup>
<br />
<br />
<InformationEdit
<UserInformationEdit
defaultData={{
name,
phone,
@ -68,7 +68,8 @@ const AccountPane = React.memo(
</Header>
</Divider>
<div className={styles.action}>
<UsernameEditPopup
<UserUsernameEditPopup
usePasswordConfirmation
defaultData={usernameUpdateForm.data}
username={username}
isSubmitting={usernameUpdateForm.isSubmitting}
@ -81,10 +82,11 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</UsernameEditPopup>
</UserUsernameEditPopup>
</div>
<div className={styles.action}>
<EmailEditPopup
<UserEmailEditPopup
usePasswordConfirmation
defaultData={emailUpdateForm.data}
email={email}
isSubmitting={emailUpdateForm.isSubmitting}
@ -97,10 +99,11 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</EmailEditPopup>
</UserEmailEditPopup>
</div>
<div className={styles.action}>
<PasswordEditPopup
<UserPasswordEditPopup
usePasswordConfirmation
defaultData={passwordUpdateForm.data}
isSubmitting={passwordUpdateForm.isSubmitting}
error={passwordUpdateForm.error}
@ -112,7 +115,7 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</PasswordEditPopup>
</UserPasswordEditPopup>
</div>
</Tab.Pane>
);

View file

@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import UserUsernameEditStep from './UserUsernameEditStep';
export default withPopup(UserUsernameEditStep);

View file

@ -1,15 +1,15 @@
import omit from 'lodash/omit';
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 { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
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 './UsernameEditPopup.module.scss';
import styles from './UserUsernameEditStep.module.scss';
const createMessage = (error) => {
if (!error) {
@ -35,8 +35,18 @@ const createMessage = (error) => {
}
};
const UsernameEditStep = React.memo(
({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const UserUsernameEditStep = React.memo(
({
defaultData,
username,
isSubmitting,
error,
usePasswordConfirmation,
onUpdate,
onMessageDismiss,
onBack,
onClose,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@ -58,7 +68,7 @@ const UsernameEditStep = React.memo(
username: data.username.trim() || null,
};
if (cleanData.username && !isUsername(cleanData.username)) {
if (!cleanData.username || !isUsername(cleanData.username)) {
usernameField.current.select();
return;
}
@ -68,13 +78,13 @@ const UsernameEditStep = React.memo(
return;
}
if (!cleanData.currentPassword) {
if (usePasswordConfirmation && !cleanData.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(cleanData);
}, [username, onUpdate, onClose, data]);
onUpdate(usePasswordConfirmation ? cleanData : omit(cleanData, 'currentPassword'));
}, [username, usePasswordConfirmation, onUpdate, onClose, data]);
useEffect(() => {
usernameField.current.select();
@ -110,7 +120,7 @@ const UsernameEditStep = React.memo(
return (
<>
<Popup.Header>
<Popup.Header onBack={onBack}>
{t('common.editUsername', {
context: 'title',
})}
@ -138,6 +148,8 @@ const UsernameEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
{usePasswordConfirmation && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
@ -147,6 +159,8 @@ const UsernameEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<Button
positive
content={t('action.save')}
@ -160,19 +174,23 @@ const UsernameEditStep = React.memo(
},
);
UsernameEditStep.propTypes = {
UserUsernameEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
username: PropTypes.string,
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
usePasswordConfirmation: PropTypes.bool,
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
UsernameEditStep.defaultProps = {
UserUsernameEditStep.defaultProps = {
username: undefined,
error: undefined,
usePasswordConfirmation: false,
onBack: undefined,
};
export default withPopup(UsernameEditStep);
export default UserUsernameEditStep;

View file

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

View file

@ -1,56 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Radio, Table } from 'semantic-ui-react';
import DeletePopup from '../DeletePopup';
import styles from './Item.module.scss';
const Item = React.memo(({ name, username, email, isAdmin, onUpdate, onDelete }) => {
const [t] = useTranslation();
const handleIsAdminChange = useCallback(() => {
onUpdate({
isAdmin: !isAdmin,
});
}, [isAdmin, onUpdate]);
return (
<Table.Row>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>{username || '-'}</Table.Cell>
<Table.Cell>{email}</Table.Cell>
<Table.Cell collapsing>
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} />
</Table.Cell>
<Table.Cell collapsing>
<DeletePopup
title={t('common.deleteUser', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisUser')}
buttonContent={t('action.deleteUser')}
onConfirm={onDelete}
>
<Button content={t('action.delete')} className={styles.button} />
</DeletePopup>
</Table.Cell>
</Table.Row>
);
});
Item.propTypes = {
name: PropTypes.string.isRequired,
username: PropTypes.string,
email: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
Item.defaultProps = {
username: undefined,
};
export default Item;

View file

@ -0,0 +1,181 @@
import pick from 'lodash/pick';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../../lib/popup';
import { Popup } from '../../../lib/custom-ui';
import { useSteps } from '../../../hooks';
import UserInformationEditStep from '../../UserInformationEditStep';
import UserUsernameEditStep from '../../UserUsernameEditStep';
import UserEmailEditStep from '../../UserEmailEditStep';
import UserPasswordEditStep from '../../UserPasswordEditStep';
import DeleteStep from '../../DeleteStep';
import styles from './ActionsPopup.module.scss';
const StepTypes = {
EDIT_INFORMATION: 'EDIT_INFORMATION',
EDIT_USERNAME: 'EDIT_USERNAME',
EDIT_EMAIL: 'EDIT_EMAIL',
EDIT_PASSWORD: 'EDIT_PASSWORD',
DELETE: 'DELETE',
};
const ActionsStep = React.memo(
({
user,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onDelete,
onClose,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleEditInformationClick = useCallback(() => {
openStep(StepTypes.EDIT_INFORMATION);
}, [openStep]);
const handleEditUsernameClick = useCallback(() => {
openStep(StepTypes.EDIT_USERNAME);
}, [openStep]);
const handleEditEmailClick = useCallback(() => {
openStep(StepTypes.EDIT_EMAIL);
}, [openStep]);
const handleEditPasswordClick = useCallback(() => {
openStep(StepTypes.EDIT_PASSWORD);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step) {
switch (step.type) {
case StepTypes.EDIT_INFORMATION:
return (
<UserInformationEditStep
defaultData={pick(user, ['name', 'phone', 'organization'])}
onUpdate={onUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_USERNAME:
return (
<UserUsernameEditStep
defaultData={user.usernameUpdateForm.data}
username={user.username}
isSubmitting={user.usernameUpdateForm.isSubmitting}
error={user.usernameUpdateForm.error}
onUpdate={onUsernameUpdate}
onMessageDismiss={onUsernameUpdateMessageDismiss}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_EMAIL:
return (
<UserEmailEditStep
defaultData={user.emailUpdateForm.data}
email={user.email}
isSubmitting={user.emailUpdateForm.isSubmitting}
error={user.emailUpdateForm.error}
onUpdate={onEmailUpdate}
onMessageDismiss={onEmailUpdateMessageDismiss}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_PASSWORD:
return (
<UserPasswordEditStep
defaultData={user.passwordUpdateForm.data}
isSubmitting={user.passwordUpdateForm.isSubmitting}
error={user.emailUpdateForm.error}
onUpdate={onPasswordUpdate}
onMessageDismiss={onPasswordUpdateMessageDismiss}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.DELETE:
return (
<DeleteStep
title={t('common.deleteUser', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisUser')}
buttonContent={t('action.deleteUser')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
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={handleEditInformationClick}>
{t('action.editInformation', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
{t('action.editEmail', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditPasswordClick}>
{t('action.editPassword', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteUser', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
ActionsStep.propTypes = {
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: 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,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);

View file

@ -0,0 +1,11 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}

View file

@ -0,0 +1,103 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { Button, Icon, Radio, Table } from 'semantic-ui-react';
import ActionsPopup from './ActionsPopup';
import styles from './Item.module.scss';
const Item = React.memo(
({
email,
username,
name,
organization,
phone,
isAdmin,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onDelete,
}) => {
const handleIsAdminChange = useCallback(() => {
onUpdate({
isAdmin: !isAdmin,
});
}, [isAdmin, onUpdate]);
return (
<Table.Row>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>{username || '-'}</Table.Cell>
<Table.Cell>{email}</Table.Cell>
<Table.Cell collapsing>
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} />
</Table.Cell>
<Table.Cell collapsing>
<ActionsPopup
user={{
email,
username,
name,
organization,
phone,
isAdmin,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
}}
onUpdate={onUpdate}
onUsernameUpdate={onUsernameUpdate}
onUsernameUpdateMessageDismiss={onUsernameUpdateMessageDismiss}
onEmailUpdate={onEmailUpdate}
onEmailUpdateMessageDismiss={onEmailUpdateMessageDismiss}
onPasswordUpdate={onPasswordUpdate}
onPasswordUpdateMessageDismiss={onPasswordUpdateMessageDismiss}
onDelete={onDelete}
>
<Button className={styles.button}>
<Icon fitted name="pencil" />
</Button>
</ActionsPopup>
</Table.Cell>
</Table.Row>
);
},
);
Item.propTypes = {
email: PropTypes.string.isRequired,
username: PropTypes.string,
name: PropTypes.string.isRequired,
organization: PropTypes.string,
phone: PropTypes.string,
isAdmin: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
emailUpdateForm: PropTypes.object.isRequired,
passwordUpdateForm: PropTypes.object.isRequired,
usernameUpdateForm: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
onUpdate: 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,
onDelete: PropTypes.func.isRequired,
};
Item.defaultProps = {
username: undefined,
organization: undefined,
phone: undefined,
};
export default Item;

View file

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

View file

@ -6,7 +6,19 @@ import { Button, Modal, Table } from 'semantic-ui-react';
import UserAddPopupContainer from '../../containers/UserAddPopupContainer';
import Item from './Item';
const UsersModal = React.memo(({ items, onUpdate, onDelete, onClose }) => {
const UsersModal = React.memo(
({
items,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onDelete,
onClose,
}) => {
const [t] = useTranslation();
const handleUpdate = useCallback(
@ -16,6 +28,48 @@ const UsersModal = React.memo(({ items, onUpdate, onDelete, onClose }) => {
[onUpdate],
);
const handleUsernameUpdate = useCallback(
(id, data) => {
onUsernameUpdate(id, data);
},
[onUsernameUpdate],
);
const handleUsernameUpdateMessageDismiss = useCallback(
(id) => {
onUsernameUpdateMessageDismiss(id);
},
[onUsernameUpdateMessageDismiss],
);
const handleEmailUpdate = useCallback(
(id, data) => {
onEmailUpdate(id, data);
},
[onEmailUpdate],
);
const handleEmailUpdateMessageDismiss = useCallback(
(id) => {
onEmailUpdateMessageDismiss(id);
},
[onEmailUpdateMessageDismiss],
);
const handlePasswordUpdate = useCallback(
(id, data) => {
onPasswordUpdate(id, data);
},
[onPasswordUpdate],
);
const handlePasswordUpdateMessageDismiss = useCallback(
(id) => {
onPasswordUpdateMessageDismiss(id);
},
[onPasswordUpdateMessageDismiss],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
@ -45,11 +99,22 @@ const UsersModal = React.memo(({ items, onUpdate, onDelete, onClose }) => {
{items.map((item) => (
<Item
key={item.id}
name={item.name}
username={item.username}
email={item.email}
username={item.username}
name={item.name}
organization={item.organization}
phone={item.phone}
isAdmin={item.isAdmin}
emailUpdateForm={item.emailUpdateForm}
passwordUpdateForm={item.passwordUpdateForm}
usernameUpdateForm={item.usernameUpdateForm}
onUpdate={(data) => handleUpdate(item.id, data)}
onUsernameUpdate={(data) => handleUsernameUpdate(item.id, data)}
onUsernameUpdateMessageDismiss={() => handleUsernameUpdateMessageDismiss(item.id)}
onEmailUpdate={(data) => handleEmailUpdate(item.id, data)}
onEmailUpdateMessageDismiss={() => handleEmailUpdateMessageDismiss(item.id)}
onPasswordUpdate={(data) => handlePasswordUpdate(item.id, data)}
onPasswordUpdateMessageDismiss={() => handlePasswordUpdateMessageDismiss(item.id)}
onDelete={() => handleDelete(item.id)}
/>
))}
@ -63,11 +128,18 @@ const UsersModal = React.memo(({ items, onUpdate, onDelete, onClose }) => {
</Modal.Actions>
</Modal>
);
});
},
);
UsersModal.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: 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,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

View file

@ -31,11 +31,17 @@ export default {
USER_UPDATE: `${PREFIX}/USER_UPDATE`,
CURRENT_USER_UPDATE: `${PREFIX}/CURRENT_USER_UPDATE`,
USER_UPDATE_HANDLE: `${PREFIX}/USER_UPDATE_HANDLE`,
USER_EMAIL_UPDATE: `${PREFIX}/USER_EMAIL_UPDATE`,
CURRENT_USER_EMAIL_UPDATE: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE`,
USER_EMAIL_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_EMAIL_UPDATE_ERROR_CLEAR`,
CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR`,
USER_PASSWORD_UPDATE: `${PREFIX}/USER_PASSWORD_UPDATE`,
CURRENT_USER_PASSWORD_UPDATE: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE`,
USER_PASSWORD_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_PASSWORD_UPDATE_ERROR_CLEAR`,
CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR`,
USER_USERNAME_UPDATE: `${PREFIX}/USER_USERNAME_UPDATE`,
CURRENT_USER_USERNAME_UPDATE: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE`,
USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_USERNAME_UPDATE_ERROR_CLEAR`,
CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR`,
CURRENT_USER_AVATAR_UPDATE: `${PREFIX}/CURRENT_USER_AVATAR_UPDATE`,
USER_DELETE: `${PREFIX}/USER_DELETE`,

View file

@ -2,7 +2,17 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { usersExceptCurrentSelector } from '../selectors';
import { closeModal, deleteUser, updateUser } from '../actions/entry';
import {
clearUserEmailUpdateError,
clearUserPasswordUpdateError,
clearUserUsernameUpdateError,
closeModal,
deleteUser,
updateUser,
updateUserEmail,
updateUserPassword,
updateUserUsername,
} from '../actions/entry';
import UsersModal from '../components/UsersModal';
const mapStateToProps = (state) => {
@ -17,6 +27,12 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
onUpdate: updateUser,
onUsernameUpdate: updateUserUsername,
onUsernameUpdateMessageDismiss: clearUserUsernameUpdateError,
onEmailUpdate: updateUserEmail,
onEmailUpdateMessageDismiss: clearUserEmailUpdateError,
onPasswordUpdate: updateUserPassword,
onPasswordUpdateMessageDismiss: clearUserPasswordUpdateError,
onDelete: deleteUser,
onClose: closeModal,
},

View file

@ -70,6 +70,7 @@ export default {
editBoard_title: 'Edit Board',
editDueDate_title: 'Edit Due Date',
editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information',
editLabel_title: 'Edit Label',
editPassword_title: 'Edit Password',
editTimer_title: 'Edit Timer',
@ -178,6 +179,7 @@ export default {
editDueDate_title: 'Edit Due Date',
editDescription_title: 'Edit Description',
editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information',
editPassword_title: 'Edit Password',
editTimer_title: 'Edit Timer',
editTitle_title: 'Edit Title',

View file

@ -35,6 +35,7 @@ export default class extends Model {
static fields = {
id: attr(),
email: attr(),
username: attr(),
name: attr(),
avatarUrl: attr(),
phone: attr(),
@ -140,6 +141,18 @@ export default class extends Model {
break;
}
case ActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
emailUpdateForm: {
...userModel.emailUpdateForm,
error: null,
},
});
break;
}
case ActionTypes.USER_PASSWORD_UPDATE: {
const userModel = User.withId(payload.id);
@ -174,6 +187,18 @@ export default class extends Model {
break;
}
case ActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
passwordUpdateForm: {
...userModel.passwordUpdateForm,
error: null,
},
});
break;
}
case ActionTypes.USER_USERNAME_UPDATE: {
const userModel = User.withId(payload.id);
@ -208,6 +233,18 @@ export default class extends Model {
break;
}
case ActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
usernameUpdateForm: {
...userModel.usernameUpdateForm,
error: null,
},
});
break;
}
case ActionTypes.USER_AVATAR_UPDATE:
User.withId(payload.id).update({
isAvatarUpdating: true,

View file

@ -8,8 +8,16 @@ import {
clearCurrentUserPasswordUpdateErrorService,
clearCurrentUserUsernameUpdateErrorService,
clearUserCreateErrorService,
clearUserEmailUpdateErrorService,
clearUserPasswordUpdateErrorService,
clearUserUsernameUpdateErrorService,
createUserService,
deleteUserService,
handleUserCreateService,
handleUserDeleteService,
handleUserFromCardRemoveService,
handleUserToCardAddService,
handleUserUpdateService,
removeUserFromCardService,
removeUserFromCurrentCardService,
removeUserFromFilterInCurrentBoardService,
@ -19,11 +27,9 @@ import {
updateCurrentUserPasswordService,
updateCurrentUserService,
updateCurrentUserUsernameService,
handleUserCreateService,
handleUserUpdateService,
handleUserDeleteService,
handleUserToCardAddService,
handleUserFromCardRemoveService,
updateUserEmailService,
updateUserPasswordService,
updateUserUsernameService,
} from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
@ -43,21 +49,39 @@ export default function* userWatchers() {
takeEvery(EntryActionTypes.USER_UPDATE_HANDLE, ({ payload: { user } }) =>
handleUserUpdateService(user),
),
takeEvery(EntryActionTypes.USER_EMAIL_UPDATE, ({ payload: { id, data } }) =>
updateUserEmailService(id, data),
),
takeEvery(EntryActionTypes.CURRENT_USER_EMAIL_UPDATE, ({ payload: { data } }) =>
updateCurrentUserEmailService(data),
),
takeEvery(EntryActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR, ({ payload: { id } }) =>
clearUserEmailUpdateErrorService(id),
),
takeEvery(EntryActionTypes.CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR, () =>
clearCurrentUserEmailUpdateErrorService(),
),
takeEvery(EntryActionTypes.USER_PASSWORD_UPDATE, ({ payload: { id, data } }) =>
updateUserPasswordService(id, data),
),
takeEvery(EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE, ({ payload: { data } }) =>
updateCurrentUserPasswordService(data),
),
takeEvery(EntryActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR, ({ payload: { id } }) =>
clearUserPasswordUpdateErrorService(id),
),
takeEvery(EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR, () =>
clearCurrentUserPasswordUpdateErrorService(),
),
takeEvery(EntryActionTypes.USER_USERNAME_UPDATE, ({ payload: { id, data } }) =>
updateUserUsernameService(id, data),
),
takeEvery(EntryActionTypes.CURRENT_USER_USERNAME_UPDATE, ({ payload: { data } }) =>
updateCurrentUserUsernameService(data),
),
takeEvery(EntryActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR, ({ payload: { id } }) =>
clearUserUsernameUpdateErrorService(id),
),
takeEvery(EntryActionTypes.CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR, () =>
clearCurrentUserUsernameUpdateErrorService(),
),