mirror of https://gitee.com/answerdev/answer.git
feat: add change user role modal
This commit is contained in:
parent
81ec786366
commit
a6ace8b38d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue