mirror of https://gitee.com/answerdev/answer.git
feat(ui): admin module update
This commit is contained in:
parent
4f977549db
commit
3c6151b806
|
@ -976,6 +976,10 @@ ui:
|
|||
tos: Terms of Service
|
||||
privacy: Privacy
|
||||
seo: SEO
|
||||
customize: Customize
|
||||
themes: Themes
|
||||
css_and_html: CSS/HTML
|
||||
login: Login
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
@ -1079,6 +1083,29 @@ ui:
|
|||
change_status: Change status
|
||||
change_role: Change role
|
||||
show_logs: Show logs
|
||||
add_user: Add user
|
||||
new_password_modal:
|
||||
title: Set new password
|
||||
form:
|
||||
fields:
|
||||
password:
|
||||
label: Password
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
user_modal:
|
||||
title: Add new user
|
||||
form:
|
||||
fields:
|
||||
display_name:
|
||||
label: Display Name
|
||||
email:
|
||||
label: Email
|
||||
msg: Email is not valid.
|
||||
password:
|
||||
label: Password
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
|
||||
questions:
|
||||
page_title: Questions
|
||||
normal: Normal
|
||||
|
@ -1230,6 +1257,41 @@ ui:
|
|||
robots:
|
||||
label: robots.txt
|
||||
text: This will permanently override any related site settings.
|
||||
themes:
|
||||
page_title: Themes
|
||||
themes:
|
||||
label: Themes
|
||||
text: Select an existing theme.
|
||||
navbar_style:
|
||||
label: Navbar Style
|
||||
text: Select an existing theme.
|
||||
primary_color:
|
||||
label: Primary Color
|
||||
text: Modify the colors used by your themes
|
||||
css_and_html:
|
||||
page_title: CSS and HTML
|
||||
custom_css:
|
||||
label: Custom CSS
|
||||
text: This will insert as <link>
|
||||
head:
|
||||
label: Head
|
||||
text: This will insert before </head>
|
||||
header:
|
||||
label: Header
|
||||
text: This will insert after <body>
|
||||
footer:
|
||||
label: Footer
|
||||
text: This will insert before </html>.
|
||||
login:
|
||||
page_title: Login
|
||||
membership:
|
||||
label: Membership
|
||||
labelAlias: Allow new registrations
|
||||
text: Turn off to prevent anyone from creating a new account.
|
||||
private:
|
||||
label: Private
|
||||
text: Only logged in users can access this community.
|
||||
|
||||
form:
|
||||
empty: cannot be empty
|
||||
invalid: is invalid
|
||||
|
|
|
@ -53,6 +53,17 @@ export const ADMIN_NAV_MENUS = [
|
|||
name: 'flags',
|
||||
// badgeContent: 5,
|
||||
},
|
||||
{
|
||||
name: 'customize',
|
||||
children: [
|
||||
{
|
||||
name: 'themes',
|
||||
},
|
||||
{
|
||||
name: 'css_and_html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
children: [
|
||||
|
@ -63,6 +74,7 @@ export const ADMIN_NAV_MENUS = [
|
|||
{ name: 'legal' },
|
||||
{ name: 'write' },
|
||||
{ name: 'seo' },
|
||||
{ name: 'login' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { FC } from 'react';
|
||||
import {
|
||||
ForwardRefRenderFunction,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { Form, Button, Stack } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -69,8 +73,13 @@ interface IProps {
|
|||
schema: JSONSchema;
|
||||
uiSchema?: UISchema;
|
||||
formData?: Type.FormDataType;
|
||||
hiddenSubmit?: boolean;
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
interface IRef {
|
||||
validator: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,13 +90,17 @@ interface IProps {
|
|||
* @param onChange change event
|
||||
* @param onSubmit submit event
|
||||
*/
|
||||
const SchemaForm: FC<IProps> = ({
|
||||
schema,
|
||||
uiSchema = {},
|
||||
formData = {},
|
||||
onChange,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||
{
|
||||
schema,
|
||||
uiSchema = {},
|
||||
formData = {},
|
||||
onChange,
|
||||
onSubmit,
|
||||
hiddenSubmit = false,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'form',
|
||||
});
|
||||
|
@ -188,9 +201,7 @@ const SchemaForm: FC<IProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const validator = async (): Promise<boolean> => {
|
||||
const errors = requiredValidator();
|
||||
if (errors.length > 0) {
|
||||
formData = errors.reduce((acc, cur) => {
|
||||
|
@ -207,7 +218,7 @@ const SchemaForm: FC<IProps> = ({
|
|||
if (onChange instanceof Function) {
|
||||
onChange({ ...formData });
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const syncErrors = await syncValidator();
|
||||
if (syncErrors.length > 0) {
|
||||
|
@ -223,8 +234,18 @@ const SchemaForm: FC<IProps> = ({
|
|||
if (onChange instanceof Function) {
|
||||
onChange({ ...formData });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const isValid = await validator();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(formData).forEach((key) => {
|
||||
formData[key].isInvalid = false;
|
||||
formData[key].errorMsg = '';
|
||||
|
@ -232,7 +253,9 @@ const SchemaForm: FC<IProps> = ({
|
|||
if (onChange instanceof Function) {
|
||||
onChange(formData);
|
||||
}
|
||||
onSubmit(e);
|
||||
if (onSubmit instanceof Function) {
|
||||
onSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadChange = (name: string, value: string) => {
|
||||
|
@ -260,6 +283,10 @@ const SchemaForm: FC<IProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
validator,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
{keys.map((key) => {
|
||||
|
@ -312,7 +339,7 @@ const SchemaForm: FC<IProps> = ({
|
|||
required
|
||||
type={widget}
|
||||
name={key}
|
||||
id={String(item)}
|
||||
id={`form-${String(item)}`}
|
||||
label={properties[key].enumNames?.[index]}
|
||||
checked={formData[key]?.value === item}
|
||||
feedback={formData[key]?.errorMsg}
|
||||
|
@ -342,7 +369,7 @@ const SchemaForm: FC<IProps> = ({
|
|||
<Form.Label>{title}</Form.Label>
|
||||
<Form.Check
|
||||
required
|
||||
id={title}
|
||||
id={`switch-${title}`}
|
||||
name={key}
|
||||
type="switch"
|
||||
label={label}
|
||||
|
@ -419,7 +446,7 @@ const SchemaForm: FC<IProps> = ({
|
|||
if (widget === 'textarea') {
|
||||
return (
|
||||
<Form.Group
|
||||
controlId={key}
|
||||
controlId={`form-${key}`}
|
||||
key={key}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
|
@ -468,9 +495,11 @@ const SchemaForm: FC<IProps> = ({
|
|||
</Form.Group>
|
||||
);
|
||||
})}
|
||||
<Button variant="primary" type="submit">
|
||||
{t('btn_submit')}
|
||||
</Button>
|
||||
{!hiddenSubmit && (
|
||||
<Button variant="primary" type="submit">
|
||||
{t('btn_submit')}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -488,4 +517,4 @@ export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
|||
return formData;
|
||||
};
|
||||
|
||||
export default SchemaForm;
|
||||
export default forwardRef(SchemaForm);
|
||||
|
|
|
@ -6,6 +6,8 @@ import useChangeModal from './useChangeModal';
|
|||
import useEditStatusModal from './useEditStatusModal';
|
||||
import useChangeUserRoleModal from './useChangeUserRoleModal';
|
||||
import useHeadInfo from './useHeadInfo';
|
||||
import useUserModal from './useUserModal';
|
||||
import useChangePasswordModal from './useChangePasswordModal';
|
||||
|
||||
export {
|
||||
useTagModal,
|
||||
|
@ -16,4 +18,6 @@ export {
|
|||
useEditStatusModal,
|
||||
useChangeUserRoleModal,
|
||||
useHeadInfo,
|
||||
useUserModal,
|
||||
useChangePasswordModal,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import { useLayoutEffect, useState, useRef } from 'react';
|
||||
import { Modal, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
|
||||
|
||||
const div = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(div);
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
onConfirm?: (formData: any) => void;
|
||||
}
|
||||
const useChangePasswordModal = (props: IProps = {}) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.users.new_password_modal',
|
||||
});
|
||||
|
||||
const { title = t('title'), onConfirm } = props;
|
||||
const [visible, setVisibleState] = useState(false);
|
||||
const schema: JSONSchema = {
|
||||
title: t('title'),
|
||||
required: ['password'],
|
||||
properties: {
|
||||
new_password: {
|
||||
type: 'string',
|
||||
title: t('form.fields.password.label'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
new_password: {
|
||||
'ui:options': {
|
||||
type: 'password',
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState<Type.FormDataType>(
|
||||
initFormData(schema),
|
||||
);
|
||||
|
||||
const formRef = useRef<{
|
||||
validator: () => Promise<boolean>;
|
||||
}>(null);
|
||||
|
||||
const onClose = () => {
|
||||
setVisibleState(false);
|
||||
};
|
||||
|
||||
const onShow = () => {
|
||||
setVisibleState(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const isValid = await formRef.current?.validator();
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onConfirm instanceof Function) {
|
||||
onConfirm({
|
||||
slug_name: formData.slugName.value,
|
||||
display_name: formData.displayName.value,
|
||||
original_text: formData.description.value,
|
||||
});
|
||||
setFormData({
|
||||
displayName: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
slugName: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
description: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
root.render(
|
||||
<Modal show={visible} title={title} onHide={onClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title as="h5">{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SchemaForm
|
||||
ref={formRef}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onChange={handleOnChange}
|
||||
hiddenSubmit
|
||||
/>
|
||||
</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 useChangePasswordModal;
|
|
@ -0,0 +1,150 @@
|
|||
import { useLayoutEffect, useState, useRef } from 'react';
|
||||
import { Modal, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import pattern from '@/common/pattern';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
|
||||
|
||||
const div = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(div);
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
onConfirm?: (formData: any) => void;
|
||||
}
|
||||
const useAddUserModal = (props: IProps = {}) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.users.user_modal',
|
||||
});
|
||||
|
||||
const { title = t('title'), onConfirm } = props;
|
||||
const [visible, setVisibleState] = useState(false);
|
||||
const schema: JSONSchema = {
|
||||
title: t('title'),
|
||||
required: ['display_name', 'email', 'password'],
|
||||
properties: {
|
||||
display_name: {
|
||||
type: 'string',
|
||||
title: t('form.fields.display_name.label'),
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
title: t('form.fields.email.label'),
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
title: t('form.fields.password.label'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
email: {
|
||||
'ui:options': {
|
||||
type: 'email',
|
||||
validator: (value) => {
|
||||
if (value && !pattern.email.test(value)) {
|
||||
return t('form.fields.email.msg');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
password: {
|
||||
'ui:options': {
|
||||
type: 'password',
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState<Type.FormDataType>(
|
||||
initFormData(schema),
|
||||
);
|
||||
|
||||
const formRef = useRef<{
|
||||
validator: () => Promise<boolean>;
|
||||
}>(null);
|
||||
|
||||
const onClose = () => {
|
||||
setVisibleState(false);
|
||||
};
|
||||
|
||||
const onShow = () => {
|
||||
setVisibleState(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const isValid = await formRef.current?.validator();
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onConfirm instanceof Function) {
|
||||
onConfirm({
|
||||
slug_name: formData.slugName.value,
|
||||
display_name: formData.displayName.value,
|
||||
original_text: formData.description.value,
|
||||
});
|
||||
setFormData({
|
||||
displayName: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
slugName: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
description: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
root.render(
|
||||
<Modal show={visible} title={title} onHide={onClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title as="h5">{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SchemaForm
|
||||
ref={formRef}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onChange={handleOnChange}
|
||||
hiddenSubmit
|
||||
/>
|
||||
</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 useAddUserModal;
|
|
@ -0,0 +1,119 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { getSeoSetting, putSeoSetting } from '@/services';
|
||||
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||
import { useToast } from '@/hooks';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.css_and_html',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const schema: JSONSchema = {
|
||||
title: t('page_title'),
|
||||
properties: {
|
||||
custom_css: {
|
||||
type: 'string',
|
||||
title: t('custom_css.label'),
|
||||
description: t('custom_css.text'),
|
||||
},
|
||||
head: {
|
||||
type: 'string',
|
||||
title: t('head.label'),
|
||||
description: t('head.text'),
|
||||
},
|
||||
header: {
|
||||
type: 'string',
|
||||
title: t('header.label'),
|
||||
description: t('header.text'),
|
||||
},
|
||||
footer: {
|
||||
type: 'string',
|
||||
title: t('footer.label'),
|
||||
description: t('footer.text'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
custom_css: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
},
|
||||
},
|
||||
head: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
},
|
||||
},
|
||||
header: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const reqParams: Type.AdminSettingsSeo = {
|
||||
robots: formData.robots.value,
|
||||
};
|
||||
|
||||
putSeoSetting(reqParams)
|
||||
.then(() => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSeoSetting().then((setting) => {
|
||||
if (setting) {
|
||||
const formMeta = { ...formData };
|
||||
formMeta.robots.value = setting.robots;
|
||||
setFormData(formMeta);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
uiSchema={uiSchema}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,93 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { getSeoSetting, putSeoSetting } from '@/services';
|
||||
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||
import { useToast } from '@/hooks';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.login',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const schema: JSONSchema = {
|
||||
title: t('page_title'),
|
||||
properties: {
|
||||
membership: {
|
||||
type: 'boolean',
|
||||
title: t('membership.label'),
|
||||
label: t('membership.labelAlias'),
|
||||
description: t('membership.text'),
|
||||
default: true,
|
||||
},
|
||||
private: {
|
||||
type: 'string',
|
||||
title: t('private.label'),
|
||||
description: t('private.text'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
membership: {
|
||||
'ui:widget': 'switch',
|
||||
},
|
||||
private: {
|
||||
'ui:widget': 'switch',
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const reqParams: Type.AdminSettingsSeo = {
|
||||
robots: formData.robots.value,
|
||||
};
|
||||
|
||||
putSeoSetting(reqParams)
|
||||
.then(() => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSeoSetting().then((setting) => {
|
||||
if (setting) {
|
||||
const formMeta = { ...formData };
|
||||
formMeta.robots.value = setting.robots;
|
||||
setFormData(formMeta);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
uiSchema={uiSchema}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,105 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { getSeoSetting, putSeoSetting } from '@/services';
|
||||
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||
import { useToast } from '@/hooks';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.themes',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const schema: JSONSchema = {
|
||||
title: t('page_title'),
|
||||
properties: {
|
||||
themes: {
|
||||
type: 'string',
|
||||
title: t('themes.label'),
|
||||
description: t('themes.text'),
|
||||
enum: ['default'],
|
||||
enumNames: ['Default'],
|
||||
},
|
||||
navbar_style: {
|
||||
type: 'string',
|
||||
title: t('navbar_style.label'),
|
||||
description: t('navbar_style.text'),
|
||||
enum: ['colored', 'light'],
|
||||
enumNames: ['Colored', 'Light'],
|
||||
},
|
||||
primary_color: {
|
||||
type: 'string',
|
||||
title: t('primary_color.label'),
|
||||
description: t('primary_color.text'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
themes: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
navbar_style: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
primary_color: {
|
||||
'ui:options': {
|
||||
type: 'color',
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const reqParams: Type.AdminSettingsSeo = {
|
||||
robots: formData.robots.value,
|
||||
};
|
||||
|
||||
putSeoSetting(reqParams)
|
||||
.then(() => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSeoSetting().then((setting) => {
|
||||
if (setting) {
|
||||
const formMeta = { ...formData };
|
||||
formMeta.robots.value = setting.robots;
|
||||
setFormData(formMeta);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
uiSchema={uiSchema}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -1,5 +1,5 @@
|
|||
import { FC } from 'react';
|
||||
import { Form, Table, Dropdown } from 'react-bootstrap';
|
||||
import { Form, Table, Dropdown, Button } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -14,7 +14,13 @@ import {
|
|||
Icon,
|
||||
} from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useChangeModal, useChangeUserRoleModal, useToast } from '@/hooks';
|
||||
import {
|
||||
useUserModal,
|
||||
useChangeModal,
|
||||
useChangeUserRoleModal,
|
||||
useChangePasswordModal,
|
||||
useToast,
|
||||
} from '@/hooks';
|
||||
import { useQueryUsers } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { formatCount } from '@/utils';
|
||||
|
@ -44,6 +50,8 @@ const Users: FC = () => {
|
|||
const curQuery = urlSearchParams.get('query') || '';
|
||||
const currentUser = loggedUserInfoStore((state) => state.user);
|
||||
const Toast = useToast();
|
||||
const userModal = useUserModal();
|
||||
const changePasswordModal = useChangePasswordModal();
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
|
@ -99,12 +107,21 @@ const Users: FC = () => {
|
|||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<QueryGroup
|
||||
data={UserFilterKeys}
|
||||
currentSort={curFilter}
|
||||
sortKey="filter"
|
||||
i18nKeyPrefix="admin.users"
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
className="me-3"
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => userModal.onShow()}>
|
||||
{t('add_user')}
|
||||
</Button>
|
||||
<QueryGroup
|
||||
data={UserFilterKeys}
|
||||
currentSort={curFilter}
|
||||
sortKey="filter"
|
||||
i18nKeyPrefix="admin.users"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Control
|
||||
size="sm"
|
||||
|
@ -184,6 +201,10 @@ const Users: FC = () => {
|
|||
<Icon name="three-dots-vertical" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={() => changePasswordModal.onShow()}>
|
||||
{t('set_new_password')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('status', user)}>
|
||||
{t('change_status')}
|
||||
|
|
|
@ -237,6 +237,14 @@ const routes: RouteNode[] = [
|
|||
path: 'flags',
|
||||
page: 'pages/Admin/Flags',
|
||||
},
|
||||
{
|
||||
path: 'themes',
|
||||
page: 'pages/Admin/Themes',
|
||||
},
|
||||
{
|
||||
path: 'css_and_html',
|
||||
page: 'pages/Admin/CssAndHtml',
|
||||
},
|
||||
{
|
||||
path: 'general',
|
||||
page: 'pages/Admin/General',
|
||||
|
@ -277,6 +285,10 @@ const routes: RouteNode[] = [
|
|||
path: 'seo',
|
||||
page: 'pages/Admin/Seo',
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
page: 'pages/Admin/Login',
|
||||
},
|
||||
],
|
||||
},
|
||||
// for review
|
||||
|
|
Loading…
Reference in New Issue