feat(ui): Add label management

This commit is contained in:
robin 2022-11-11 14:30:36 +08:00
parent ac71eef3e9
commit 89029b1684
5 changed files with 271 additions and 1 deletions

View File

@ -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

View File

@ -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',

View File

@ -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;

View File

@ -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;

View File

@ -263,6 +263,10 @@ const routes: RouteNode[] = [
path: 'write',
page: 'pages/Admin/Write',
},
{
path: 'labels',
page: 'pages/Admin/Labels',
},
],
},
{