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_cancel: Cancel
|
||||||
btn_submit: Submit
|
btn_submit: Submit
|
||||||
btn_next: Next
|
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:
|
users:
|
||||||
title: Users
|
title: Users
|
||||||
name: Name
|
name: Name
|
||||||
|
@ -1027,15 +1040,21 @@ ui:
|
||||||
delete_at: Deleted Time
|
delete_at: Deleted Time
|
||||||
suspend_at: Suspended Time
|
suspend_at: Suspended Time
|
||||||
status: Status
|
status: Status
|
||||||
|
role: Role
|
||||||
action: Action
|
action: Action
|
||||||
change: Change
|
change: Change
|
||||||
all: All
|
all: All
|
||||||
|
staff: Staff
|
||||||
inactive: Inactive
|
inactive: Inactive
|
||||||
suspended: Suspended
|
suspended: Suspended
|
||||||
deleted: Deleted
|
deleted: Deleted
|
||||||
normal: Normal
|
normal: Normal
|
||||||
filter:
|
filter:
|
||||||
placeholder: "Filter by name, user:id"
|
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:
|
questions:
|
||||||
page_title: Questions
|
page_title: Questions
|
||||||
normal: Normal
|
normal: Normal
|
||||||
|
|
|
@ -246,7 +246,12 @@ export type AdminAnswerStatus = 'available' | 'deleted';
|
||||||
/**
|
/**
|
||||||
* @description interface for Users
|
* @description interface for Users
|
||||||
*/
|
*/
|
||||||
export type UserFilterBy = 'all' | 'inactive' | 'suspended' | 'deleted';
|
export type UserFilterBy =
|
||||||
|
| 'all'
|
||||||
|
| 'staff'
|
||||||
|
| 'inactive'
|
||||||
|
| 'suspended'
|
||||||
|
| 'deleted';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description interface for Flags
|
* @description interface for Flags
|
||||||
|
|
|
@ -11,11 +11,13 @@ interface Props {
|
||||||
showReputation?: boolean;
|
showReputation?: boolean;
|
||||||
avatarSearchStr?: string;
|
avatarSearchStr?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
avatarClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Index: FC<Props> = ({
|
const Index: FC<Props> = ({
|
||||||
data,
|
data,
|
||||||
showAvatar = true,
|
showAvatar = true,
|
||||||
|
avatarClass = '',
|
||||||
avatarSize = '20px',
|
avatarSize = '20px',
|
||||||
className = 'fs-14',
|
className = 'fs-14',
|
||||||
avatarSearchStr = 's=48',
|
avatarSearchStr = 's=48',
|
||||||
|
@ -29,7 +31,7 @@ const Index: FC<Props> = ({
|
||||||
<Avatar
|
<Avatar
|
||||||
avatar={data?.avatar}
|
avatar={data?.avatar}
|
||||||
size={avatarSize}
|
size={avatarSize}
|
||||||
className="me-1"
|
className={`me-1 ${avatarClass}`}
|
||||||
searchStr={avatarSearchStr}
|
searchStr={avatarSearchStr}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -41,7 +43,7 @@ const Index: FC<Props> = ({
|
||||||
<Avatar
|
<Avatar
|
||||||
avatar={data?.avatar}
|
avatar={data?.avatar}
|
||||||
size={avatarSize}
|
size={avatarSize}
|
||||||
className="me-1"
|
className={`me-1 ${avatarClass}`}
|
||||||
searchStr={avatarSearchStr}
|
searchStr={avatarSearchStr}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import useReportModal from './useReportModal';
|
||||||
import usePageUsers from './usePageUsers';
|
import usePageUsers from './usePageUsers';
|
||||||
import useChangeModal from './useChangeModal';
|
import useChangeModal from './useChangeModal';
|
||||||
import useEditStatusModal from './useEditStatusModal';
|
import useEditStatusModal from './useEditStatusModal';
|
||||||
|
import useChangeUserRoleModal from './useChangeUserRoleModal';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useTagModal,
|
useTagModal,
|
||||||
|
@ -12,4 +13,5 @@ export {
|
||||||
usePageUsers,
|
usePageUsers,
|
||||||
useChangeModal,
|
useChangeModal,
|
||||||
useEditStatusModal,
|
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 { 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 { useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -9,15 +9,17 @@ 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 { useChangeModal } from '@/hooks';
|
import { useChangeModal, useChangeUserRoleModal } from '@/hooks';
|
||||||
import { useQueryUsers } from '@/services';
|
import { useQueryUsers } from '@/services';
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
const UserFilterKeys: Type.UserFilterBy[] = [
|
const UserFilterKeys: Type.UserFilterBy[] = [
|
||||||
'all',
|
'all',
|
||||||
|
'staff',
|
||||||
'inactive',
|
'inactive',
|
||||||
'suspended',
|
'suspended',
|
||||||
'deleted',
|
'deleted',
|
||||||
|
@ -52,6 +54,10 @@ const Users: FC = () => {
|
||||||
callback: refreshUsers,
|
callback: refreshUsers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const changeUserRoleModal = useChangeUserRoleModal({
|
||||||
|
callback: refreshUsers,
|
||||||
|
});
|
||||||
|
|
||||||
const handleClick = ({ user_id, status }) => {
|
const handleClick = ({ user_id, status }) => {
|
||||||
changeModal.onShow({
|
changeModal.onShow({
|
||||||
id: user_id,
|
id: user_id,
|
||||||
|
@ -59,6 +65,13 @@ const Users: FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClickRole = ({ user_id, role }) => {
|
||||||
|
changeUserRoleModal.onShow({
|
||||||
|
id: user_id,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilter = (e) => {
|
const handleFilter = (e) => {
|
||||||
urlSearchParams.set('query', e.target.value);
|
urlSearchParams.set('query', e.target.value);
|
||||||
urlSearchParams.delete('page');
|
urlSearchParams.delete('page');
|
||||||
|
@ -87,20 +100,23 @@ const Users: FC = () => {
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t('name')}</th>
|
<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 style={{ width: '20%' }}>{t('email')}</th>
|
||||||
<th className="text-nowrap" style={{ width: '15%' }}>
|
<th className="text-nowrap" style={{ width: '15%' }}>
|
||||||
{t('created_at')}
|
{t('created_at')}
|
||||||
</th>
|
</th>
|
||||||
{(curFilter === 'deleted' || curFilter === 'suspended') && (
|
{(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')}
|
{curFilter === 'deleted' ? t('delete_at') : t('suspend_at')}
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<th style={{ width: '10%' }}>{t('status')}</th>
|
<th style={{ width: '12%' }}>{t('status')}</th>
|
||||||
|
<th style={{ width: '12%' }}>{t('role')}</th>
|
||||||
{curFilter !== 'deleted' ? (
|
{curFilter !== 'deleted' ? (
|
||||||
<th style={{ width: '10%' }}>{t('action')}</th>
|
<th style={{ width: '8%' }} className="text-end">
|
||||||
|
{t('action')}
|
||||||
|
</th>
|
||||||
) : null}
|
) : null}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -112,11 +128,13 @@ const Users: FC = () => {
|
||||||
<BaseUserCard
|
<BaseUserCard
|
||||||
data={user}
|
data={user}
|
||||||
className="fs-6"
|
className="fs-6"
|
||||||
avatarSize="24px"
|
avatarSize="32px"
|
||||||
avatarSearchStr="s=48"
|
avatarSearchStr="s=48"
|
||||||
|
avatarClass="me-2"
|
||||||
|
showReputation={false}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{user.rank}</td>
|
{/* <td>{user.rank}</td> */}
|
||||||
<td className="text-break">{user.e_mail}</td>
|
<td className="text-break">{user.e_mail}</td>
|
||||||
<td>
|
<td>
|
||||||
<FormatTime time={user.created_at} />
|
<FormatTime time={user.created_at} />
|
||||||
|
@ -134,16 +152,40 @@ const Users: FC = () => {
|
||||||
<td>
|
<td>
|
||||||
<Badge bg={bgMap[user.status]}>{t(user.status)}</Badge>
|
<Badge bg={bgMap[user.status]}>{t(user.status)}</Badge>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<Badge bg="light" className="text-body">
|
||||||
|
Admin
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
{curFilter !== 'deleted' ? (
|
{curFilter !== 'deleted' ? (
|
||||||
<td>
|
<td className="text-end">
|
||||||
{user.status !== 'deleted' && (
|
<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
|
<Button
|
||||||
className="p-0 btn-no-border"
|
className="p-0 btn-no-border"
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => handleClick(user)}>
|
onClick={() => handleClick(user)}>
|
||||||
{t('change')}
|
{t('change')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
</td>
|
</td>
|
||||||
) : null}
|
) : null}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
Loading…
Reference in New Issue