mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-0.6.0' into 'test'
Feat/ui 0.6.0 See merge request opensource/answer!300
This commit is contained in:
commit
044f37ae6c
|
@ -701,6 +701,7 @@ ui:
|
|||
update: update success
|
||||
update_password: Password changed successfully.
|
||||
flag_success: Thanks for flagging.
|
||||
fobidden_operate_self: Forbidden to operate on yourself
|
||||
related_question:
|
||||
title: Related Questions
|
||||
btn: Add question
|
||||
|
@ -1041,6 +1042,10 @@ ui:
|
|||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
btn_next: Next
|
||||
user_role_modal:
|
||||
title: Change user role to...
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
users:
|
||||
title: Users
|
||||
name: Name
|
||||
|
@ -1050,15 +1055,24 @@ 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
|
||||
Moderator: Moderator
|
||||
Admin: Admin
|
||||
User: User
|
||||
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
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"customize-cra": "^1.0.0",
|
||||
|
|
|
@ -15,7 +15,6 @@ specifiers:
|
|||
'@types/qs': ^6.9.7
|
||||
'@types/react': ^18.0.17
|
||||
'@types/react-dom': ^18.0.6
|
||||
'@types/react-helmet': ^6.1.5
|
||||
'@typescript-eslint/eslint-plugin': ^5.0.0
|
||||
'@typescript-eslint/parser': ^5.33.0
|
||||
axios: ^0.27.2
|
||||
|
@ -109,7 +108,6 @@ devDependencies:
|
|||
'@types/qs': 6.9.7
|
||||
'@types/react': 18.0.20
|
||||
'@types/react-dom': 18.0.6
|
||||
'@types/react-helmet': 6.1.5
|
||||
'@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry
|
||||
'@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha
|
||||
customize-cra: 1.0.0
|
||||
|
@ -2581,12 +2579,6 @@ packages:
|
|||
'@types/react': 18.0.20
|
||||
dev: true
|
||||
|
||||
/@types/react-helmet/6.1.5:
|
||||
resolution: {integrity: sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==}
|
||||
dependencies:
|
||||
'@types/react': 18.0.20
|
||||
dev: true
|
||||
|
||||
/@types/react-transition-group/4.4.5:
|
||||
resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==}
|
||||
dependencies:
|
||||
|
|
|
@ -252,7 +252,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
|
||||
|
@ -439,3 +444,9 @@ export interface ReviewResp {
|
|||
count: number;
|
||||
list: ReviewItem[];
|
||||
}
|
||||
|
||||
export interface UserRoleItem {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
|
|
@ -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,122 @@
|
|||
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 { getUserRoles, changeUserRole } from '@/services';
|
||||
import { UserRoleItem } from '@/common/interface';
|
||||
|
||||
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 [defaultId, setDefaultId] = useState(-1);
|
||||
const [isInvalid, setInvalidState] = useState(false);
|
||||
const [changedId, setChangeId] = useState(-1);
|
||||
const [show, setShow] = useState(false);
|
||||
const [list, setList] = useState<UserRoleItem[]>([]);
|
||||
|
||||
const getRolesData = async () => {
|
||||
const res = await getUserRoles();
|
||||
setList(res);
|
||||
};
|
||||
|
||||
const handleRadio = (val) => {
|
||||
setInvalidState(false);
|
||||
setChangeId(val.id);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setChangeId(-1);
|
||||
setDefaultId(-1);
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (defaultId === changedId) {
|
||||
onClose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
changeUserRole({
|
||||
user_id: id,
|
||||
role_id: changedId,
|
||||
}).then(() => {
|
||||
callback?.();
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
|
||||
const onShow = (params) => {
|
||||
getRolesData();
|
||||
setId(params.id);
|
||||
setChangeId(params.role_id);
|
||||
setDefaultId(params.role_id);
|
||||
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?.id}>
|
||||
<Form.Group controlId={item.name} className="mb-3">
|
||||
<FormCheck>
|
||||
<FormCheck.Input
|
||||
id={item.name}
|
||||
type="radio"
|
||||
checked={changedId === item.id}
|
||||
onChange={() => handleRadio(item)}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
<FormCheck.Label htmlFor={item.name}>
|
||||
<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 } from 'react-bootstrap';
|
||||
import { Form, Table, Dropdown } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -11,15 +11,18 @@ import {
|
|||
BaseUserCard,
|
||||
Empty,
|
||||
QueryGroup,
|
||||
Icon,
|
||||
} from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useChangeModal } from '@/hooks';
|
||||
import { useChangeModal, useChangeUserRoleModal, useToast } from '@/hooks';
|
||||
import { useQueryUsers } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
const UserFilterKeys: Type.UserFilterBy[] = [
|
||||
'all',
|
||||
'staff',
|
||||
'inactive',
|
||||
'suspended',
|
||||
'deleted',
|
||||
|
@ -40,6 +43,8 @@ const Users: FC = () => {
|
|||
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
|
||||
const curPage = Number(urlSearchParams.get('page') || '1');
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
const currentUser = loggedUserInfoStore((state) => state.user);
|
||||
const Toast = useToast();
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
|
@ -48,17 +53,42 @@ const Users: FC = () => {
|
|||
page: curPage,
|
||||
page_size: PAGE_SIZE,
|
||||
query: curQuery,
|
||||
...(curFilter === 'all' ? {} : { status: curFilter }),
|
||||
...(curFilter === 'all'
|
||||
? {}
|
||||
: curFilter === 'staff'
|
||||
? { staff: true }
|
||||
: { status: curFilter }),
|
||||
});
|
||||
const changeModal = useChangeModal({
|
||||
callback: refreshUsers,
|
||||
});
|
||||
|
||||
const handleClick = ({ user_id, status }) => {
|
||||
changeModal.onShow({
|
||||
id: user_id,
|
||||
type: status,
|
||||
});
|
||||
const changeUserRoleModal = useChangeUserRoleModal({
|
||||
callback: refreshUsers,
|
||||
});
|
||||
|
||||
const handleAction = (type, user) => {
|
||||
const { user_id, status, role_id, username } = user;
|
||||
if (username === currentUser.username) {
|
||||
Toast.onShow({
|
||||
msg: t('fobidden_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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilter = (e) => {
|
||||
|
@ -89,20 +119,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>
|
||||
|
@ -114,11 +147,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} />
|
||||
|
@ -138,16 +173,40 @@ const Users: FC = () => {
|
|||
{t(user.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="badge text-bg-light">
|
||||
{t(user.role_name)}
|
||||
</span>
|
||||
</td>
|
||||
{curFilter !== 'deleted' ? (
|
||||
<td>
|
||||
{user.status !== 'deleted' && (
|
||||
<td className="text-end">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="link" className="no-toggle">
|
||||
<Icon name="three-dots-vertical" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{/* <Dropdown.Item>{t('set_new_password')}</Dropdown.Item> */}
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('status', user)}>
|
||||
{t('change_status')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('role', 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>
|
||||
|
|
|
@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, introduction, data }) => {
|
|||
<h5 className="mb-3">{t('about_me')}</h5>
|
||||
{introduction ? (
|
||||
<div
|
||||
className="mb-4"
|
||||
className="mb-4 text-break"
|
||||
dangerouslySetInnerHTML={{ __html: introduction }}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -21,3 +21,11 @@ export const useQueryUsers = (params) => {
|
|||
mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUserRoles = () => {
|
||||
return request.get('/answer/admin/api/roles');
|
||||
};
|
||||
|
||||
export const changeUserRole = (params) => {
|
||||
return request.put('/answer/admin/api/user/role', params);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue