mirror of https://gitee.com/answerdev/answer.git
feat(ui): Add label management
This commit is contained in:
parent
ac71eef3e9
commit
89029b1684
|
@ -917,6 +917,7 @@ ui:
|
|||
smtp: SMTP
|
||||
branding: Branding
|
||||
legal: Legal
|
||||
labels: Labels
|
||||
dashboard:
|
||||
title: Dashboard
|
||||
welcome: Welcome to Answer Admin!
|
||||
|
@ -1123,6 +1124,26 @@ ui:
|
|||
label: Privacy Policy
|
||||
msg: Privacy policy cannot be empty.
|
||||
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here."
|
||||
labels:
|
||||
title: Labels
|
||||
new_label: New Label
|
||||
name: Name
|
||||
color: color
|
||||
description: Description
|
||||
action: Action
|
||||
form:
|
||||
title: Create New Label
|
||||
back: Back
|
||||
display_name:
|
||||
label: Display Name
|
||||
url_slug:
|
||||
label: URL Slug
|
||||
text: Must use the character set “a-z”, “0-9”, “+ # - .”
|
||||
description:
|
||||
label: Description (optional)
|
||||
color:
|
||||
label: Color
|
||||
|
||||
form:
|
||||
empty: cannot be empty
|
||||
invalid: is invalid
|
||||
|
|
|
@ -43,7 +43,7 @@ export const ADMIN_NAV_MENUS = [
|
|||
},
|
||||
{
|
||||
name: 'contents',
|
||||
child: [{ name: 'questions' }, { name: 'answers' }],
|
||||
child: [{ name: 'questions' }, { name: 'answers' }, { name: 'labels' }],
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
||||
|
||||
interface IProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
const LabelForm: FC<IProps> = ({ onClose }) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.labels.form',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const updateSiteInfo = siteInfoStore((state) => state.update);
|
||||
|
||||
const { data: setting } = useGeneralSetting();
|
||||
const schema: JSONSchema = {
|
||||
title: t('title'),
|
||||
required: ['name', 'site_url', 'contact_email'],
|
||||
properties: {
|
||||
display_name: {
|
||||
type: 'string',
|
||||
title: t('display_name.label'),
|
||||
},
|
||||
url_slug: {
|
||||
type: 'string',
|
||||
title: t('url_slug.label'),
|
||||
description: t('url_slug.text'),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
title: t('description.label'),
|
||||
description: t('description.text'),
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
title: t('color.label'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
color: {
|
||||
'ui:options': {
|
||||
type: 'color',
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const reqParams: Type.AdminSettingsGeneral = {
|
||||
name: formData.name.value,
|
||||
description: formData.description.value,
|
||||
short_description: formData.short_description.value,
|
||||
site_url: formData.site_url.value,
|
||||
contact_email: formData.contact_email.value,
|
||||
};
|
||||
|
||||
updateGeneralSetting(reqParams)
|
||||
.then(() => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
onClose();
|
||||
updateSiteInfo(reqParams);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError && err.key) {
|
||||
formData[err.key].isInvalid = true;
|
||||
formData[err.key].errorMsg = err.value;
|
||||
}
|
||||
setFormData({ ...formData });
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
const formMeta = {};
|
||||
Object.keys(setting).forEach((k) => {
|
||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||
});
|
||||
setFormData({ ...formData, ...formMeta });
|
||||
}, [setting]);
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="mb-4"
|
||||
variant="outline-secondary"
|
||||
onClick={onClose}>
|
||||
← {t('back')}
|
||||
</Button>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
uiSchema={uiSchema}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelForm;
|
|
@ -0,0 +1,124 @@
|
|||
import { FC, useState } from 'react';
|
||||
import { Button, Table } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Pagination, BaseUserCard, Empty } from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useChangeModal } from '@/hooks';
|
||||
import { useQueryUsers } from '@/services';
|
||||
|
||||
import CreateForm from './Form';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
const UserFilterKeys: Type.UserFilterBy[] = [
|
||||
'all',
|
||||
'inactive',
|
||||
'suspended',
|
||||
'deleted',
|
||||
];
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const Users: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.labels' });
|
||||
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
|
||||
const curPage = Number(urlSearchParams.get('page') || '1');
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
|
||||
const [isCreate, setCreateState] = useState(true);
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
mutate: refreshUsers,
|
||||
} = useQueryUsers({
|
||||
page: curPage,
|
||||
page_size: PAGE_SIZE,
|
||||
query: curQuery,
|
||||
...(curFilter === 'all' ? {} : { status: curFilter }),
|
||||
});
|
||||
const changeModal = useChangeModal({
|
||||
callback: refreshUsers,
|
||||
});
|
||||
|
||||
const handleClick = ({ user_id, status }) => {
|
||||
changeModal.onShow({
|
||||
id: user_id,
|
||||
type: status,
|
||||
});
|
||||
};
|
||||
|
||||
if (isCreate) {
|
||||
return <CreateForm onClose={() => setCreateState(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
size="sm"
|
||||
onClick={() => setCreateState(true)}>
|
||||
{t('new_label')}
|
||||
</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('name')}</th>
|
||||
<th style={{ width: '12%' }}>{t('color')}</th>
|
||||
<th style={{ width: '20%' }}>{t('description')}</th>
|
||||
|
||||
{curFilter !== 'deleted' ? (
|
||||
<th style={{ width: '10%' }}>{t('action')}</th>
|
||||
) : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="align-middle">
|
||||
{data?.list.map((user) => {
|
||||
return (
|
||||
<tr key={user.user_id}>
|
||||
<td>
|
||||
<BaseUserCard
|
||||
data={user}
|
||||
className="fs-6"
|
||||
avatarSize="24px"
|
||||
avatarSearchStr="s=48"
|
||||
/>
|
||||
</td>
|
||||
<td>{user.rank}</td>
|
||||
<td className="text-break">{user.e_mail}</td>
|
||||
|
||||
{curFilter !== 'deleted' ? (
|
||||
<td>
|
||||
{user.status !== 'deleted' && (
|
||||
<Button
|
||||
className="p-0 btn-no-border"
|
||||
variant="link"
|
||||
onClick={() => handleClick(user)}>
|
||||
{t('change')}
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
) : null}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
{Number(data?.count) <= 0 && !isLoading && <Empty />}
|
||||
<div className="mt-4 mb-2 d-flex justify-content-center">
|
||||
<Pagination
|
||||
currentPage={curPage}
|
||||
totalSize={data?.count || 0}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
|
@ -263,6 +263,10 @@ const routes: RouteNode[] = [
|
|||
path: 'write',
|
||||
page: 'pages/Admin/Write',
|
||||
},
|
||||
{
|
||||
path: 'labels',
|
||||
page: 'pages/Admin/Labels',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue