feat: add change user role modal

This commit is contained in:
shuai 2022-11-28 16:00:29 +08:00
parent 81ec786366
commit a6ace8b38d
6 changed files with 278 additions and 14 deletions

View File

@ -1018,6 +1018,19 @@ ui:
btn_cancel: Cancel
btn_submit: Submit
btn_next: Next
user_role_modal:
title: Change user role to...
btn_cancel: Cancel
btn_submit: Submit
user:
name: User
description: Default with no special access.
admin:
name: Admin
description: Have the full power to access the site.
moderator:
name: Moderator
description: Has access to all posts except admin settings.
users:
title: Users
name: Name
@ -1027,15 +1040,21 @@ ui:
delete_at: Deleted Time
suspend_at: Suspended Time
status: Status
role: Role
action: Action
change: Change
all: All
staff: Staff
inactive: Inactive
suspended: Suspended
deleted: Deleted
normal: Normal
filter:
placeholder: "Filter by name, user:id"
set_new_password: Set new password
change_status: Change status
change_role: Change role
show_logs: Show logs
questions:
page_title: Questions
normal: Normal

View File

@ -246,7 +246,12 @@ export type AdminAnswerStatus = 'available' | 'deleted';
/**
* @description interface for Users
*/
export type UserFilterBy = 'all' | 'inactive' | 'suspended' | 'deleted';
export type UserFilterBy =
| 'all'
| 'staff'
| 'inactive'
| 'suspended'
| 'deleted';
/**
* @description interface for Flags

View File

@ -11,11 +11,13 @@ interface Props {
showReputation?: boolean;
avatarSearchStr?: string;
className?: string;
avatarClass?: string;
}
const Index: FC<Props> = ({
data,
showAvatar = true,
avatarClass = '',
avatarSize = '20px',
className = 'fs-14',
avatarSearchStr = 's=48',
@ -29,7 +31,7 @@ const Index: FC<Props> = ({
<Avatar
avatar={data?.avatar}
size={avatarSize}
className="me-1"
className={`me-1 ${avatarClass}`}
searchStr={avatarSearchStr}
/>
)}
@ -41,7 +43,7 @@ const Index: FC<Props> = ({
<Avatar
avatar={data?.avatar}
size={avatarSize}
className="me-1"
className={`me-1 ${avatarClass}`}
searchStr={avatarSearchStr}
/>
)}

View File

@ -4,6 +4,7 @@ import useReportModal from './useReportModal';
import usePageUsers from './usePageUsers';
import useChangeModal from './useChangeModal';
import useEditStatusModal from './useEditStatusModal';
import useChangeUserRoleModal from './useChangeUserRoleModal';
export {
useTagModal,
@ -12,4 +13,5 @@ export {
usePageUsers,
useChangeModal,
useEditStatusModal,
useChangeUserRoleModal,
};

View File

@ -0,0 +1,194 @@
import { useLayoutEffect, useState } from 'react';
import { Modal, Form, Button, FormCheck } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client';
import { Modal as AnswerModal } from '@/components';
import { changeUserStatus } from '@/services';
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
interface Props {
callback?: () => void;
}
const useChangeUserRoleModal = ({ callback }: Props) => {
const { t } = useTranslation('translation', {
keyPrefix: 'admin.user_role_modal',
});
const [id, setId] = useState('');
const [defaultType, setDefaultType] = useState('');
const [isInvalid, setInvalidState] = useState(false);
const [changeType, setChangeType] = useState({
type: '',
haveContent: false,
});
const [content, setContent] = useState({
value: '',
isInvalid: false,
errorMsg: '',
});
const [show, setShow] = useState(false);
const [list] = useState<any[]>([
{
type: 'user',
name: t('user.name'),
description: t('user.description'),
},
{
type: 'admin',
name: t('admin.name'),
description: t('admin.description'),
},
{
type: 'moderator',
name: t('moderator.name'),
description: t('moderator.description'),
},
]);
const handleRadio = (val) => {
setInvalidState(false);
setContent({
value: '',
isInvalid: false,
errorMsg: '',
});
setChangeType({
type: val.type,
haveContent: val.have_content,
});
};
const onClose = () => {
setChangeType({
type: '',
haveContent: false,
});
setContent({
value: '',
isInvalid: false,
errorMsg: '',
});
setContent({
value: '',
isInvalid: false,
errorMsg: '',
});
setShow(false);
};
const handleSubmit = () => {
if (changeType.type === '') {
setInvalidState(true);
return;
}
if (changeType.haveContent && !content.value) {
setContent({
value: content.value,
isInvalid: true,
errorMsg: t('remark.empty'),
});
return;
}
if (defaultType === changeType.type) {
onClose();
return;
}
if (changeType.type === 'deleted') {
onClose();
AnswerModal.confirm({
title: t('confirm_title'),
content: t('confirm_content'),
confirmText: t('confirm_btn'),
confirmBtnVariant: 'danger',
onConfirm: () => {
changeUserStatus({
user_id: id,
status: changeType.type,
}).then(() => {
callback?.();
onClose();
});
},
});
return;
}
changeUserStatus({
user_id: id,
status: changeType.type,
}).then(() => {
callback?.();
onClose();
});
};
const onShow = (params) => {
setId(params.id);
setChangeType({
...changeType,
type: params.type,
});
setDefaultType(params.type);
setShow(true);
};
useLayoutEffect(() => {
root.render(
<Modal show={show} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title as="h5">{t('title')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
{list.map((item) => {
return (
<div key={item?.type}>
<Form.Group controlId={item.type} className="mb-3">
<FormCheck>
<FormCheck.Input
id={item.type}
type="radio"
checked={changeType.type === item.type}
onChange={() => handleRadio(item)}
isInvalid={isInvalid}
/>
<FormCheck.Label htmlFor={item.type}>
<span className="fw-bold">{item.name}</span>
<br />
<span className="text-secondary">
{item.description}
</span>
</FormCheck.Label>
<Form.Control.Feedback type="invalid">
{t('msg.empty')}
</Form.Control.Feedback>
</FormCheck>
</Form.Group>
</div>
);
})}
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="link" onClick={() => onClose()}>
{t('btn_cancel')}
</Button>
<Button variant="primary" onClick={handleSubmit}>
{t('btn_submit')}
</Button>
</Modal.Footer>
</Modal>,
);
});
return {
onClose,
onShow,
};
};
export default useChangeUserRoleModal;

View File

@ -1,5 +1,5 @@
import { FC } from 'react';
import { Button, Form, Table, Badge } from 'react-bootstrap';
import { Form, Table, Badge, Dropdown } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@ -9,15 +9,17 @@ import {
BaseUserCard,
Empty,
QueryGroup,
Icon,
} from '@/components';
import * as Type from '@/common/interface';
import { useChangeModal } from '@/hooks';
import { useChangeModal, useChangeUserRoleModal } from '@/hooks';
import { useQueryUsers } from '@/services';
import '../index.scss';
const UserFilterKeys: Type.UserFilterBy[] = [
'all',
'staff',
'inactive',
'suspended',
'deleted',
@ -52,6 +54,10 @@ const Users: FC = () => {
callback: refreshUsers,
});
const changeUserRoleModal = useChangeUserRoleModal({
callback: refreshUsers,
});
const handleClick = ({ user_id, status }) => {
changeModal.onShow({
id: user_id,
@ -59,6 +65,13 @@ const Users: FC = () => {
});
};
const handleClickRole = ({ user_id, role }) => {
changeUserRoleModal.onShow({
id: user_id,
role,
});
};
const handleFilter = (e) => {
urlSearchParams.set('query', e.target.value);
urlSearchParams.delete('page');
@ -87,20 +100,23 @@ const Users: FC = () => {
<thead>
<tr>
<th>{t('name')}</th>
<th style={{ width: '12%' }}>{t('reputation')}</th>
{/* <th style={{ width: '12%' }}>{t('reputation')}</th> */}
<th style={{ width: '20%' }}>{t('email')}</th>
<th className="text-nowrap" style={{ width: '15%' }}>
{t('created_at')}
</th>
{(curFilter === 'deleted' || curFilter === 'suspended') && (
<th className="text-nowrap" style={{ width: '10%' }}>
<th className="text-nowrap" style={{ width: '15%' }}>
{curFilter === 'deleted' ? t('delete_at') : t('suspend_at')}
</th>
)}
<th style={{ width: '10%' }}>{t('status')}</th>
<th style={{ width: '12%' }}>{t('status')}</th>
<th style={{ width: '12%' }}>{t('role')}</th>
{curFilter !== 'deleted' ? (
<th style={{ width: '10%' }}>{t('action')}</th>
<th style={{ width: '8%' }} className="text-end">
{t('action')}
</th>
) : null}
</tr>
</thead>
@ -112,11 +128,13 @@ const Users: FC = () => {
<BaseUserCard
data={user}
className="fs-6"
avatarSize="24px"
avatarSize="32px"
avatarSearchStr="s=48"
avatarClass="me-2"
showReputation={false}
/>
</td>
<td>{user.rank}</td>
{/* <td>{user.rank}</td> */}
<td className="text-break">{user.e_mail}</td>
<td>
<FormatTime time={user.created_at} />
@ -134,16 +152,40 @@ const Users: FC = () => {
<td>
<Badge bg={bgMap[user.status]}>{t(user.status)}</Badge>
</td>
<td>
<Badge bg="light" className="text-body">
Admin
</Badge>
</td>
{curFilter !== 'deleted' ? (
<td>
{user.status !== 'deleted' && (
<td className="text-end">
<Dropdown>
<Dropdown.Toggle
variant="link"
className="no-toggle link-secondary">
<Icon name="three-dots-vertical" />
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item>{t('set_new_password')}</Dropdown.Item>
<Dropdown.Item onClick={() => handleClick(user)}>
{t('change_status')}
</Dropdown.Item>
<Dropdown.Item onClick={() => handleClickRole(user)}>
{t('change_role')}
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item>{t('show_logs')}</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
{/* {user.status !== 'deleted' && (
<Button
className="p-0 btn-no-border"
variant="link"
onClick={() => handleClick(user)}>
{t('change')}
</Button>
)}
)} */}
</td>
) : null}
</tr>