feat: simplify adjustment of user activity status

This commit is contained in:
shuai 2023-09-13 09:52:31 +08:00
parent 4a9ad54353
commit a4adf7f40d
5 changed files with 322 additions and 112 deletions

View File

@ -1119,6 +1119,10 @@ ui:
comment: Comment comment: Comment
refresh: Refresh refresh: Refresh
resend: Resend resend: Resend
deactivate: Deactivate
active: Active
suspend: Suspend
unsuspend: Unsuspend
search: search:
title: Search Results title: Search Results
keywords: Keywords keywords: Keywords
@ -1498,6 +1502,18 @@ ui:
change_role: Change role change_role: Change role
show_logs: Show logs show_logs: Show logs
add_user: Add user add_user: Add user
deactivate_user:
title: Deactivate user
content: An inactive user must re-validate their email.
delete_user:
title: Delete this user
content: Are you sure you want to delete this user? This is permanent!
remove: Remove their content
label: Remove all questions, answers, comments, etc.
text: Dont check this if you wish to only delete the users account.
suspend_user:
title: Suspend this user
content: A suspended user can't log in.
questions: questions:
page_title: Questions page_title: Questions
normal: Normal normal: Normal

View File

@ -0,0 +1,57 @@
import { useState } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
const DeleteUserModal = ({ show, onClose, onDelete }) => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
const [checkVal, setCheckVal] = useState(false);
const handleClose = () => {
onClose();
setCheckVal(false);
};
return (
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>{t('delete_user.title')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>{t('delete_user.content')}</p>
<div className="text-danger mb-2">
{t('delete_user.remove')} {t('optional', { keyPrefix: 'form' })}
</div>
<Form>
<Form.Group controlId="delete_user">
<Form.Check type="checkbox" id="delete_user">
<Form.Check.Input
type="checkbox"
checked={checkVal}
onChange={(e) => {
setCheckVal(e.target.checked);
}}
/>
<Form.Check.Label htmlFor="delete_user">
<span>{t('delete_user.label')}</span>
<br />
<span className="small text-secondary">
{t('delete_user.text')}
</span>
</Form.Check.Label>
</Form.Check>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="link" onClick={handleClose}>
{t('cancel', { keyPrefix: 'btns' })}
</Button>
<Button variant="danger" onClick={() => onDelete(checkVal)}>
{t('delete', { keyPrefix: 'btns' })}
</Button>
</Modal.Footer>
</Modal>
);
};
export default DeleteUserModal;

View File

@ -0,0 +1,194 @@
import { Dropdown } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Modal, Icon } from '@/components';
import {
useChangeUserRoleModal,
useChangePasswordModal,
useActivationEmailModal,
useToast,
} from '@/hooks';
import { updateUserPassword, changeUserStatus } from '@/services';
interface Props {
showActionPassword?: boolean;
showActionStatus?: boolean;
showActionRole?: boolean;
currentUser;
refreshUsers: () => void;
showDeleteModal: (val) => void;
userData;
}
const UserOperation = ({
showActionPassword,
showActionStatus,
showActionRole,
currentUser,
refreshUsers,
showDeleteModal,
userData,
}: Props) => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
const Toast = useToast();
const changeUserRoleModal = useChangeUserRoleModal({
callback: refreshUsers,
});
const changePasswordModal = useChangePasswordModal({
onConfirm: (rd) => {
return new Promise((resolve, reject) => {
updateUserPassword(rd)
.then(() => {
Toast.onShow({
msg: t('update_password', { keyPrefix: 'toast' }),
variant: 'success',
});
resolve(true);
})
.catch((e) => {
reject(e);
});
});
},
});
const activationEmailModal = useActivationEmailModal();
const postUserStatus = (statusType) => {
changeUserStatus({
user_id: userData.user_id,
status: statusType,
}).then(() => {
refreshUsers?.();
// onClose();
});
};
const handleAction = (type) => {
const { user_id, role_id, username } = userData;
if (username === currentUser.username) {
Toast.onShow({
msg: t('forbidden_operate_self', { keyPrefix: 'toast' }),
variant: 'warning',
});
return;
}
if (type === 'role') {
changeUserRoleModal.onShow({
id: user_id,
role_id,
});
}
if (type === 'password') {
changePasswordModal.onShow(user_id);
}
if (type === 'activation') {
activationEmailModal.onShow(user_id);
}
if (type === 'deactivate') {
// cons
Modal.confirm({
title: t('deactivate_user.title'),
content: t('deactivate_user.content'),
cancelBtnVariant: 'link',
confirmBtnVariant: 'danger',
cancelText: t('cancel', { keyPrefix: 'btns' }),
confirmText: t('deactivate', { keyPrefix: 'btns' }),
onConfirm: () => {
// active -> inactive
postUserStatus('inactive');
},
});
}
if (type === 'suspend') {
// cons
Modal.confirm({
title: t('suspend_user.title'),
content: t('suspend_user.content'),
cancelBtnVariant: 'link',
cancelText: t('cancel', { keyPrefix: 'btns' }),
confirmBtnVariant: 'danger',
confirmText: t('suspend', { keyPrefix: 'btns' }),
onConfirm: () => {
// active -> suspended
postUserStatus('suspended');
},
});
}
if (type === 'active' || type === 'unsuspend') {
// to normal
postUserStatus('normal');
}
if (type === 'delete') {
showDeleteModal({
show: true,
userId: userData.user_id,
});
}
};
return (
<td className="text-end">
<Dropdown>
<Dropdown.Toggle variant="link" className="no-toggle">
<Icon name="three-dots-vertical" title={t('action')} />
</Dropdown.Toggle>
<Dropdown.Menu>
{showActionPassword ? (
<Dropdown.Item onClick={() => handleAction('password')}>
{t('set_new_password')}
</Dropdown.Item>
) : null}
{showActionRole ? (
<Dropdown.Item onClick={() => handleAction('role')}>
{t('change_role')}
</Dropdown.Item>
) : null}
{userData.status === 'inactive' ? (
<Dropdown.Item onClick={() => handleAction('activation')}>
{t('btn_name', { keyPrefix: 'inactive' })}
</Dropdown.Item>
) : null}
{showActionStatus && userData.status !== 'deleted' ? (
<>
<Dropdown.Divider />
{userData.status === 'inactive' && (
<Dropdown.Item onClick={() => handleAction('active')}>
{t('active', { keyPrefix: 'btns' })}
</Dropdown.Item>
)}
{userData.status === 'normal' && (
<Dropdown.Item onClick={() => handleAction('deactivate')}>
{t('deactivate', { keyPrefix: 'btns' })}
</Dropdown.Item>
)}
{userData.status === 'normal' && (
<Dropdown.Item onClick={() => handleAction('suspend')}>
{t('suspend', { keyPrefix: 'btns' })}
</Dropdown.Item>
)}
{userData.status === 'suspended' && (
<Dropdown.Item onClick={() => handleAction('unsuspend')}>
{t('unsuspend', { keyPrefix: 'btns' })}
</Dropdown.Item>
)}
<Dropdown.Item onClick={() => handleAction('delete')}>
{t('delete', { keyPrefix: 'btns' })}
</Dropdown.Item>
</>
) : null}
</Dropdown.Menu>
</Dropdown>
</td>
);
};
export default UserOperation;

View File

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { Form, Table, Dropdown, Button, Stack } from 'react-bootstrap'; import { Form, Table, Button, Stack } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -11,27 +11,22 @@ import {
BaseUserCard, BaseUserCard,
Empty, Empty,
QueryGroup, QueryGroup,
Icon,
} from '@/components'; } from '@/components';
import * as Type from '@/common/interface'; import * as Type from '@/common/interface';
import { import { useUserModal } from '@/hooks';
useUserModal,
useChangeModal,
useChangeUserRoleModal,
useChangePasswordModal,
useActivationEmailModal,
useToast,
} from '@/hooks';
import { import {
useQueryUsers, useQueryUsers,
addUsers, addUsers,
updateUserPassword,
getAdminUcAgent, getAdminUcAgent,
AdminUcAgent, AdminUcAgent,
changeUserStatus,
} from '@/services'; } from '@/services';
import { loggedUserInfoStore, userCenterStore } from '@/stores'; import { loggedUserInfoStore, userCenterStore } from '@/stores';
import { formatCount } from '@/utils'; import { formatCount } from '@/utils';
import DeleteUserModal from './components/DeleteUserModal';
import UserOperate from './components/UserOperate';
const UserFilterKeys: Type.UserFilterBy[] = [ const UserFilterKeys: Type.UserFilterBy[] = [
'all', 'all',
'staff', 'staff',
@ -50,7 +45,10 @@ const bgMap = {
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
const Users: FC = () => { const Users: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' }); const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
const [deleteUserModalState, setDeleteUserModalState] = useState({
show: false,
userId: '',
});
const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0]; const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
const curPage = Number(urlSearchParams.get('page') || '1'); const curPage = Number(urlSearchParams.get('page') || '1');
@ -63,7 +61,7 @@ const Users: FC = () => {
allow_update_user_password: true, allow_update_user_password: true,
allow_update_user_role: true, allow_update_user_role: true,
}); });
const Toast = useToast();
const { const {
data, data,
isLoading, isLoading,
@ -78,13 +76,6 @@ const Users: FC = () => {
? { staff: true } ? { staff: true }
: { status: curFilter }), : { status: curFilter }),
}); });
const changeModal = useChangeModal({
callback: refreshUsers,
});
const changeUserRoleModal = useChangeUserRoleModal({
callback: refreshUsers,
});
const userModal = useUserModal({ const userModal = useUserModal({
onConfirm: (userModel) => { onConfirm: (userModel) => {
@ -102,58 +93,6 @@ const Users: FC = () => {
}); });
}, },
}); });
const changePasswordModal = useChangePasswordModal({
onConfirm: (rd) => {
return new Promise((resolve, reject) => {
updateUserPassword(rd)
.then(() => {
Toast.onShow({
msg: t('update_password', { keyPrefix: 'toast' }),
variant: 'success',
});
resolve(true);
})
.catch((e) => {
reject(e);
});
});
},
});
const activationEmailModal = useActivationEmailModal();
const handleAction = (type, user) => {
const { user_id, status, role_id, username } = user;
if (username === currentUser.username) {
Toast.onShow({
msg: t('forbidden_operate_self', { keyPrefix: 'toast' }),
variant: 'warning',
});
return;
}
if (type === 'status') {
changeModal.onShow({
id: user_id,
type: status,
});
}
if (type === 'role') {
changeUserRoleModal.onShow({
id: user_id,
role_id,
});
}
if (type === 'password') {
changePasswordModal.onShow(user_id);
}
if (type === 'activation') {
activationEmailModal.onShow(user_id);
}
};
const handleFilter = (e) => { const handleFilter = (e) => {
urlSearchParams.set('query', e.target.value); urlSearchParams.set('query', e.target.value);
@ -167,9 +106,30 @@ const Users: FC = () => {
}); });
} }
}, [ucAgent]); }, [ucAgent]);
const changeDeleteUserModalState = (modalData: {
show: boolean;
userId: string;
}) => {
setDeleteUserModalState(modalData);
};
const handleDelete = (val) => {
changeUserStatus({
user_id: deleteUserModalState.userId,
status: 'deleted',
remove_all_content: val,
}).then(() => {
changeDeleteUserModalState({
show: false,
userId: '',
});
refreshUsers();
});
};
const showAddUser = const showAddUser =
!ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user); !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user);
const showActionPassword = const showActionPassword =
!ucAgent?.enabled || !ucAgent?.enabled ||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_password); (ucAgent?.enabled && adminUcAgent?.allow_update_user_password);
@ -181,7 +141,6 @@ const Users: FC = () => {
const showActionStatus = const showActionStatus =
!ucAgent?.enabled || !ucAgent?.enabled ||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_status); (ucAgent?.enabled && adminUcAgent?.allow_update_user_status);
const showAction = showActionPassword || showActionRole || showActionStatus; const showAction = showActionPassword || showActionRole || showActionStatus;
return ( return (
@ -242,8 +201,6 @@ const Users: FC = () => {
</thead> </thead>
<tbody className="align-middle"> <tbody className="align-middle">
{data?.list.map((user) => { {data?.list.map((user) => {
const showActionActivation = user.status === 'inactive';
return ( return (
<tr key={user.user_id}> <tr key={user.user_id}>
<td> <td>
@ -285,40 +242,16 @@ const Users: FC = () => {
</td> </td>
)} )}
{curFilter !== 'deleted' && {curFilter !== 'deleted' &&
(showAction || showActionActivation) ? ( (showAction || user.status === 'inactive') ? (
<td className="text-end"> <UserOperate
<Dropdown> userData={user}
<Dropdown.Toggle variant="link" className="no-toggle"> showActionPassword={showActionPassword}
<Icon name="three-dots-vertical" title={t('action')} /> showActionRole={showActionRole}
</Dropdown.Toggle> showActionStatus={showActionStatus}
<Dropdown.Menu> currentUser={currentUser}
{showActionPassword ? ( refreshUsers={refreshUsers}
<Dropdown.Item showDeleteModal={changeDeleteUserModalState}
onClick={() => handleAction('password', user)}> />
{t('set_new_password')}
</Dropdown.Item>
) : null}
{showActionStatus ? (
<Dropdown.Item
onClick={() => handleAction('status', user)}>
{t('change_status')}
</Dropdown.Item>
) : null}
{showActionRole ? (
<Dropdown.Item
onClick={() => handleAction('role', user)}>
{t('change_role')}
</Dropdown.Item>
) : null}
{showActionActivation ? (
<Dropdown.Item
onClick={() => handleAction('activation', user)}>
{t('btn_name', { keyPrefix: 'inactive' })}
</Dropdown.Item>
) : null}
</Dropdown.Menu>
</Dropdown>
</td>
) : null} ) : null}
</tr> </tr>
); );
@ -333,6 +266,17 @@ const Users: FC = () => {
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
/> />
</div> </div>
<DeleteUserModal
show={deleteUserModalState.show}
onClose={() => {
changeDeleteUserModalState({
show: false,
userId: '',
});
}}
onDelete={(val) => handleDelete(val)}
/>
</> </>
); );
}; };

View File

@ -82,9 +82,8 @@ const Personal: FC = () => {
)} )}
</Col> </Col>
</Row> </Row>
<NavBar tabName={tabName} slug={username} isSelf={isSelf} />
<Row> <Row>
<NavBar tabName={tabName} slug={username} isSelf={isSelf} />
<Col className="page-main flex-auto"> <Col className="page-main flex-auto">
<Overview <Overview
visible={tabName === 'overview'} visible={tabName === 'overview'}