mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-0.7.0' into 'test'
Feat/ui 0.7.0 See merge request opensource/answer!329
This commit is contained in:
commit
1a453bdf72
|
@ -728,7 +728,7 @@ ui:
|
||||||
update: update success
|
update: update success
|
||||||
update_password: Password changed successfully.
|
update_password: Password changed successfully.
|
||||||
flag_success: Thanks for flagging.
|
flag_success: Thanks for flagging.
|
||||||
fobidden_operate_self: Forbidden to operate on yourself
|
forbidden_operate_self: Forbidden to operate on yourself
|
||||||
review: Your revision will show after review.
|
review: Your revision will show after review.
|
||||||
related_question:
|
related_question:
|
||||||
title: Related Questions
|
title: Related Questions
|
||||||
|
@ -1003,6 +1003,10 @@ ui:
|
||||||
tos: Terms of Service
|
tos: Terms of Service
|
||||||
privacy: Privacy
|
privacy: Privacy
|
||||||
seo: SEO
|
seo: SEO
|
||||||
|
customize: Customize
|
||||||
|
themes: Themes
|
||||||
|
css-html: CSS/HTML
|
||||||
|
login: Login
|
||||||
admin:
|
admin:
|
||||||
admin_header:
|
admin_header:
|
||||||
title: Admin
|
title: Admin
|
||||||
|
@ -1106,6 +1110,29 @@ ui:
|
||||||
change_status: Change status
|
change_status: Change status
|
||||||
change_role: Change role
|
change_role: Change role
|
||||||
show_logs: Show logs
|
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:
|
questions:
|
||||||
page_title: Questions
|
page_title: Questions
|
||||||
normal: Normal
|
normal: Normal
|
||||||
|
@ -1258,6 +1285,42 @@ ui:
|
||||||
robots:
|
robots:
|
||||||
label: robots.txt
|
label: robots.txt
|
||||||
text: This will permanently override any related site settings.
|
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:
|
||||||
|
title: Membership
|
||||||
|
label: Allow new registrations
|
||||||
|
text: Turn off to prevent anyone from creating a new account.
|
||||||
|
private:
|
||||||
|
title: Private
|
||||||
|
label: Login required
|
||||||
|
text: Only logged in users can access this community.
|
||||||
|
|
||||||
form:
|
form:
|
||||||
empty: cannot be empty
|
empty: cannot be empty
|
||||||
invalid: is invalid
|
invalid: is invalid
|
||||||
|
|
|
@ -53,6 +53,17 @@ export const ADMIN_NAV_MENUS = [
|
||||||
name: 'flags',
|
name: 'flags',
|
||||||
// badgeContent: 5,
|
// badgeContent: 5,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'customize',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'themes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'css-html',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
children: [
|
children: [
|
||||||
|
@ -63,6 +74,7 @@ export const ADMIN_NAV_MENUS = [
|
||||||
{ name: 'legal' },
|
{ name: 'legal' },
|
||||||
{ name: 'write' },
|
{ name: 'write' },
|
||||||
{ name: 'seo' },
|
{ name: 'seo' },
|
||||||
|
{ name: 'login' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -346,6 +346,28 @@ export interface AdminSettingsSeo {
|
||||||
robots: string;
|
robots: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type themeConfig = {
|
||||||
|
navbar_style: string;
|
||||||
|
primary_color: string;
|
||||||
|
[k: string]: string | number;
|
||||||
|
};
|
||||||
|
export interface AdminSettingsTheme {
|
||||||
|
theme: string;
|
||||||
|
theme_config: Record<string, themeConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminSettingsCustom {
|
||||||
|
custom_css: string;
|
||||||
|
custom_head: string;
|
||||||
|
custom_header: string;
|
||||||
|
custom_footer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminSettingsLogin {
|
||||||
|
allow_new_registrations: boolean;
|
||||||
|
login_required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description interface for Activity
|
* @description interface for Activity
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
.collapse-indicator {
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanding .collapse-indicator {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
|
@ -1,44 +1,63 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import { Accordion, Button, Stack } from 'react-bootstrap';
|
import { Accordion, Nav } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useMatch } from 'react-router-dom';
|
import { useNavigate, useMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import { useAccordionButton } from 'react-bootstrap/AccordionButton';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Icon } from '@/components';
|
import { Icon } from '@/components';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
function MenuNode({
|
||||||
|
menu,
|
||||||
|
callback,
|
||||||
|
activeKey,
|
||||||
|
expanding = false,
|
||||||
|
path = '/',
|
||||||
|
}) {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||||
const accordionClick = useAccordionButton(menu.name);
|
const isLeaf = !menu.children.length;
|
||||||
const menuOnClick = (evt) => {
|
const href = isLeaf ? `${path}${menu.name}` : '#';
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation();
|
|
||||||
if (!isLeaf) {
|
|
||||||
accordionClick(evt);
|
|
||||||
}
|
|
||||||
if (typeof callback === 'function') {
|
|
||||||
callback(menu);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let menuCls = 'text-start text-dark text-nowrap shadow-none bg-body border-0';
|
|
||||||
let menuVariant = 'light';
|
|
||||||
if (activeKey === menu.name) {
|
|
||||||
menuCls = 'text-start text-white text-nowrap shadow-none';
|
|
||||||
menuVariant = 'primary';
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Button variant={menuVariant} className={menuCls} onClick={menuOnClick}>
|
<Nav.Item key={menu.name}>
|
||||||
<Stack direction="horizontal">
|
<Nav.Link
|
||||||
{!isLeaf ? <Icon name="chevron-right" className="me-1" /> : null}
|
eventKey={menu.name}
|
||||||
{t(menu.name)}
|
as={isLeaf ? 'a' : 'button'}
|
||||||
|
onClick={(evt) => {
|
||||||
|
callback(evt, menu, href, isLeaf);
|
||||||
|
}}
|
||||||
|
href={href}
|
||||||
|
className={classNames(
|
||||||
|
'text-nowrap d-flex flex-nowrap align-items-center w-100',
|
||||||
|
{ expanding, 'link-dark': activeKey !== menu.name },
|
||||||
|
)}>
|
||||||
|
<span className="me-auto">{t(menu.name)}</span>
|
||||||
{menu.badgeContent ? (
|
{menu.badgeContent ? (
|
||||||
<span className="badge text-bg-dark ms-auto top-0">
|
<span className="badge text-bg-dark">{menu.badgeContent}</span>
|
||||||
{menu.badgeContent}
|
|
||||||
</span>
|
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
{!isLeaf && (
|
||||||
</Button>
|
<Icon className="collapse-indicator" name="chevron-right" />
|
||||||
|
)}
|
||||||
|
</Nav.Link>
|
||||||
|
{menu.children.length ? (
|
||||||
|
<Accordion.Collapse eventKey={menu.name} className="ms-3">
|
||||||
|
<>
|
||||||
|
{menu.children.map((leaf) => {
|
||||||
|
return (
|
||||||
|
<MenuNode
|
||||||
|
menu={leaf}
|
||||||
|
callback={callback}
|
||||||
|
activeKey={activeKey}
|
||||||
|
path={path}
|
||||||
|
key={leaf.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</Accordion.Collapse>
|
||||||
|
) : null}
|
||||||
|
</Nav.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,12 +65,9 @@ interface AccordionProps {
|
||||||
menus: any[];
|
menus: any[];
|
||||||
path?: string;
|
path?: string;
|
||||||
}
|
}
|
||||||
const AccordionNav: FC<AccordionProps> = ({ menus, path = '/' }) => {
|
const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const pathMatch = useMatch(`${path}*`);
|
const pathMatch = useMatch(`${path}*`);
|
||||||
if (!menus.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// auto set menu fields
|
// auto set menu fields
|
||||||
menus.forEach((m) => {
|
menus.forEach((m) => {
|
||||||
if (!Array.isArray(m.children)) {
|
if (!Array.isArray(m.children)) {
|
||||||
|
@ -68,57 +84,50 @@ const AccordionNav: FC<AccordionProps> = ({ menus, path = '/' }) => {
|
||||||
if (splat) {
|
if (splat) {
|
||||||
activeKey = splat;
|
activeKey = splat;
|
||||||
}
|
}
|
||||||
const menuClick = (clickedMenu) => {
|
const getOpenKey = () => {
|
||||||
const menuKey = clickedMenu.name;
|
let openKey = '';
|
||||||
if (clickedMenu.children.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeKey !== menuKey) {
|
|
||||||
const routePath = `${path}${menuKey}`;
|
|
||||||
navigate(routePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let defaultOpenKey;
|
|
||||||
menus.forEach((li) => {
|
menus.forEach((li) => {
|
||||||
if (li.children.length) {
|
if (li.children.length) {
|
||||||
const matchedChild = li.children.find((el) => {
|
const matchedChild = li.children.find((el) => {
|
||||||
return el.name === activeKey;
|
return el.name === activeKey;
|
||||||
});
|
});
|
||||||
if (matchedChild) {
|
if (matchedChild) {
|
||||||
defaultOpenKey = li.name;
|
openKey = li.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return openKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [openKey, setOpenKey] = useState(getOpenKey());
|
||||||
|
const menuClick = (evt, menu, href, isLeaf) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
if (isLeaf) {
|
||||||
|
navigate(href);
|
||||||
|
} else {
|
||||||
|
setOpenKey(openKey === menu.name ? '' : menu.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
setOpenKey(getOpenKey());
|
||||||
|
}, [activeKey]);
|
||||||
return (
|
return (
|
||||||
<Accordion defaultActiveKey={defaultOpenKey} flush>
|
<Accordion activeKey={openKey} flush>
|
||||||
<Stack direction="vertical" gap={1}>
|
<Nav variant="pills" className="flex-column" activeKey={activeKey}>
|
||||||
{menus.map((li) => {
|
{menus.map((li) => {
|
||||||
return (
|
|
||||||
<React.Fragment key={li.name}>
|
|
||||||
<MenuNode menu={li} callback={menuClick} activeKey={activeKey} />
|
|
||||||
{li.children.length ? (
|
|
||||||
<Accordion.Collapse eventKey={li.name} className="ms-4">
|
|
||||||
<Stack direction="vertical" gap={1}>
|
|
||||||
{li.children.map((leaf) => {
|
|
||||||
return (
|
return (
|
||||||
<MenuNode
|
<MenuNode
|
||||||
menu={leaf}
|
menu={li}
|
||||||
|
path={path}
|
||||||
callback={menuClick}
|
callback={menuClick}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
isLeaf
|
expanding={openKey === li.name}
|
||||||
key={leaf.name}
|
key={li.name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Nav>
|
||||||
</Accordion.Collapse>
|
|
||||||
) : null}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,7 +35,6 @@ const Index: FC = () => {
|
||||||
if (!tryLoggedAndActivated().ok) {
|
if (!tryLoggedAndActivated().ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isEdit ? (
|
return isEdit ? (
|
||||||
<Card className="mb-4">
|
<Card className="mb-4">
|
||||||
<Card.Header className="text-nowrap d-flex justify-content-between">
|
<Card.Header className="text-nowrap d-flex justify-content-between">
|
||||||
|
@ -80,7 +79,9 @@ const Index: FC = () => {
|
||||||
<>
|
<>
|
||||||
<div className="text-muted">{t('follow_tag_tip')}</div>
|
<div className="text-muted">{t('follow_tag_tip')}</div>
|
||||||
<NavLink className="d-inline-block my-2" to="/tags">
|
<NavLink className="d-inline-block my-2" to="/tags">
|
||||||
<Button variant="outline-primary">{t('follow_a_tag')}</Button>
|
<Button size="sm" variant="outline-primary">
|
||||||
|
{t('follow_a_tag')}
|
||||||
|
</Button>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { FC } from 'react';
|
import {
|
||||||
|
ForwardRefRenderFunction,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from 'react';
|
||||||
import { Form, Button, Stack } from 'react-bootstrap';
|
import { Form, Button, Stack } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -69,8 +73,13 @@ interface IProps {
|
||||||
schema: JSONSchema;
|
schema: JSONSchema;
|
||||||
uiSchema?: UISchema;
|
uiSchema?: UISchema;
|
||||||
formData?: Type.FormDataType;
|
formData?: Type.FormDataType;
|
||||||
|
hiddenSubmit?: boolean;
|
||||||
onChange?: (data: Type.FormDataType) => void;
|
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 onChange change event
|
||||||
* @param onSubmit submit event
|
* @param onSubmit submit event
|
||||||
*/
|
*/
|
||||||
const SchemaForm: FC<IProps> = ({
|
const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
|
{
|
||||||
schema,
|
schema,
|
||||||
uiSchema = {},
|
uiSchema = {},
|
||||||
formData = {},
|
formData = {},
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}) => {
|
hiddenSubmit = false,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'form',
|
keyPrefix: 'form',
|
||||||
});
|
});
|
||||||
|
@ -188,9 +201,7 @@ const SchemaForm: FC<IProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const validator = async (): Promise<boolean> => {
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const errors = requiredValidator();
|
const errors = requiredValidator();
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
formData = errors.reduce((acc, cur) => {
|
formData = errors.reduce((acc, cur) => {
|
||||||
|
@ -207,7 +218,7 @@ const SchemaForm: FC<IProps> = ({
|
||||||
if (onChange instanceof Function) {
|
if (onChange instanceof Function) {
|
||||||
onChange({ ...formData });
|
onChange({ ...formData });
|
||||||
}
|
}
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
const syncErrors = await syncValidator();
|
const syncErrors = await syncValidator();
|
||||||
if (syncErrors.length > 0) {
|
if (syncErrors.length > 0) {
|
||||||
|
@ -223,8 +234,18 @@ const SchemaForm: FC<IProps> = ({
|
||||||
if (onChange instanceof Function) {
|
if (onChange instanceof Function) {
|
||||||
onChange({ ...formData });
|
onChange({ ...formData });
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const isValid = await validator();
|
||||||
|
if (!isValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(formData).forEach((key) => {
|
Object.keys(formData).forEach((key) => {
|
||||||
formData[key].isInvalid = false;
|
formData[key].isInvalid = false;
|
||||||
formData[key].errorMsg = '';
|
formData[key].errorMsg = '';
|
||||||
|
@ -232,7 +253,9 @@ const SchemaForm: FC<IProps> = ({
|
||||||
if (onChange instanceof Function) {
|
if (onChange instanceof Function) {
|
||||||
onChange(formData);
|
onChange(formData);
|
||||||
}
|
}
|
||||||
|
if (onSubmit instanceof Function) {
|
||||||
onSubmit(e);
|
onSubmit(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUploadChange = (name: string, value: string) => {
|
const handleUploadChange = (name: string, value: string) => {
|
||||||
|
@ -260,6 +283,10 @@ const SchemaForm: FC<IProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
validator,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
{keys.map((key) => {
|
{keys.map((key) => {
|
||||||
|
@ -312,7 +339,7 @@ const SchemaForm: FC<IProps> = ({
|
||||||
required
|
required
|
||||||
type={widget}
|
type={widget}
|
||||||
name={key}
|
name={key}
|
||||||
id={String(item)}
|
id={`form-${String(item)}`}
|
||||||
label={properties[key].enumNames?.[index]}
|
label={properties[key].enumNames?.[index]}
|
||||||
checked={formData[key]?.value === item}
|
checked={formData[key]?.value === item}
|
||||||
feedback={formData[key]?.errorMsg}
|
feedback={formData[key]?.errorMsg}
|
||||||
|
@ -342,7 +369,7 @@ const SchemaForm: FC<IProps> = ({
|
||||||
<Form.Label>{title}</Form.Label>
|
<Form.Label>{title}</Form.Label>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
required
|
required
|
||||||
id={title}
|
id={`switch-${title}`}
|
||||||
name={key}
|
name={key}
|
||||||
type="switch"
|
type="switch"
|
||||||
label={label}
|
label={label}
|
||||||
|
@ -419,7 +446,7 @@ const SchemaForm: FC<IProps> = ({
|
||||||
if (widget === 'textarea') {
|
if (widget === 'textarea') {
|
||||||
return (
|
return (
|
||||||
<Form.Group
|
<Form.Group
|
||||||
controlId={key}
|
controlId={`form-${key}`}
|
||||||
key={key}
|
key={key}
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
||||||
<Form.Label>{title}</Form.Label>
|
<Form.Label>{title}</Form.Label>
|
||||||
|
@ -468,9 +495,11 @@ const SchemaForm: FC<IProps> = ({
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{!hiddenSubmit && (
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">
|
||||||
{t('btn_submit')}
|
{t('btn_submit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -488,4 +517,4 @@ export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SchemaForm;
|
export default forwardRef(SchemaForm);
|
||||||
|
|
|
@ -5,6 +5,8 @@ import usePageUsers from './usePageUsers';
|
||||||
import useChangeModal from './useChangeModal';
|
import useChangeModal from './useChangeModal';
|
||||||
import useEditStatusModal from './useEditStatusModal';
|
import useEditStatusModal from './useEditStatusModal';
|
||||||
import useChangeUserRoleModal from './useChangeUserRoleModal';
|
import useChangeUserRoleModal from './useChangeUserRoleModal';
|
||||||
|
import useUserModal from './useUserModal';
|
||||||
|
import useChangePasswordModal from './useChangePasswordModal';
|
||||||
import usePageTags from './usePageTags';
|
import usePageTags from './usePageTags';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -15,5 +17,7 @@ export {
|
||||||
useChangeModal,
|
useChangeModal,
|
||||||
useEditStatusModal,
|
useEditStatusModal,
|
||||||
useChangeUserRoleModal,
|
useChangeUserRoleModal,
|
||||||
|
useUserModal,
|
||||||
|
useChangePasswordModal,
|
||||||
usePageTags,
|
usePageTags,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
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 [userId, setUserId] = useState('');
|
||||||
|
const schema: JSONSchema = {
|
||||||
|
title: t('title'),
|
||||||
|
required: ['password'],
|
||||||
|
properties: {
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('form.fields.password.label'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
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 = (user_id: string) => {
|
||||||
|
setUserId(user_id);
|
||||||
|
setVisibleState(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const isValid = await formRef.current?.validator();
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onConfirm instanceof Function) {
|
||||||
|
onConfirm({
|
||||||
|
password: formData.password.value,
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
setFormData({
|
||||||
|
password: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setUserId('');
|
||||||
|
}
|
||||||
|
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({
|
||||||
|
display_name: formData.display_name.value,
|
||||||
|
email: formData.email.value,
|
||||||
|
password: formData.password.value,
|
||||||
|
});
|
||||||
|
setFormData({
|
||||||
|
display_name: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
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,125 @@
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import { getPageCustom, putPageCustom } 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'),
|
||||||
|
},
|
||||||
|
custom_head: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('head.label'),
|
||||||
|
description: t('head.text'),
|
||||||
|
},
|
||||||
|
custom_header: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('header.label'),
|
||||||
|
description: t('header.text'),
|
||||||
|
},
|
||||||
|
custom_footer: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('footer.label'),
|
||||||
|
description: t('footer.text'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
custom_css: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
custom_head: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
custom_header: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
custom_footer: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
|
||||||
|
const onSubmit = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const reqParams: Type.AdminSettingsCustom = {
|
||||||
|
custom_css: formData.custom_css.value,
|
||||||
|
custom_head: formData.custom_head.value,
|
||||||
|
custom_header: formData.custom_header.value,
|
||||||
|
custom_footer: formData.custom_footer.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
putPageCustom(reqParams)
|
||||||
|
.then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData);
|
||||||
|
setFormData({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getPageCustom().then((setting) => {
|
||||||
|
if (setting) {
|
||||||
|
const formMeta = { ...formData };
|
||||||
|
formMeta.custom_css.value = setting.custom_css;
|
||||||
|
formMeta.custom_head.value = setting.custom_head;
|
||||||
|
formMeta.custom_header.value = setting.custom_header;
|
||||||
|
formMeta.custom_footer.value = setting.custom_footer;
|
||||||
|
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,98 @@
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import { getLoginSetting, putLoginSetting } 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: {
|
||||||
|
allow_new_registrations: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: t('membership.title'),
|
||||||
|
label: t('membership.label'),
|
||||||
|
description: t('membership.text'),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
login_required: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: t('private.title'),
|
||||||
|
label: t('private.label'),
|
||||||
|
description: t('private.text'),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
allow_new_registrations: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
},
|
||||||
|
login_required: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
|
||||||
|
const onSubmit = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const reqParams: Type.AdminSettingsLogin = {
|
||||||
|
allow_new_registrations: formData.allow_new_registrations.value,
|
||||||
|
login_required: formData.login_required.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
putLoginSetting(reqParams)
|
||||||
|
.then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData);
|
||||||
|
setFormData({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLoginSetting().then((setting) => {
|
||||||
|
if (setting) {
|
||||||
|
const formMeta = { ...formData };
|
||||||
|
formMeta.allow_new_registrations.value =
|
||||||
|
setting.allow_new_registrations;
|
||||||
|
formMeta.login_required.value = setting.login_required;
|
||||||
|
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,127 @@
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import { getThemeSetting, putThemeSetting } 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'],
|
||||||
|
default: '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'),
|
||||||
|
default: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
themes: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
|
navbar_style: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
|
primary_color: {
|
||||||
|
'ui:options': {
|
||||||
|
type: 'color',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [themeSetting, setThemeSetting] = useState<Type.AdminSettingsTheme>();
|
||||||
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
const onSubmit = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const themeName = formData.themes.value;
|
||||||
|
const reqParams: Type.AdminSettingsTheme = {
|
||||||
|
theme: themeName,
|
||||||
|
theme_config: {
|
||||||
|
[themeName]: {
|
||||||
|
navbar_style: formData.navbar_style.value,
|
||||||
|
primary_color: formData.primary_color.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
putThemeSetting(reqParams)
|
||||||
|
.then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData);
|
||||||
|
setFormData({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getThemeSetting().then((setting) => {
|
||||||
|
if (setting) {
|
||||||
|
setThemeSetting(setting);
|
||||||
|
const themeName = setting.theme;
|
||||||
|
const themeConfig = setting.theme_config[themeName];
|
||||||
|
const formMeta = { ...formData };
|
||||||
|
formMeta.themes.value = themeName;
|
||||||
|
formMeta.navbar_style.value = themeConfig?.navbar_style;
|
||||||
|
formMeta.primary_color.value = themeConfig?.primary_color;
|
||||||
|
setFormData({ ...formMeta });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnChange = (cd) => {
|
||||||
|
console.log('cd: ', cd);
|
||||||
|
setFormData(cd);
|
||||||
|
const themeConfig = themeSetting?.theme_config[cd.themes.value];
|
||||||
|
if (themeConfig) {
|
||||||
|
themeConfig.navbar_style = cd.navbar_style.value;
|
||||||
|
themeConfig.primary_color = cd.primary_color.value;
|
||||||
|
setThemeSetting({
|
||||||
|
theme: themeSetting?.theme,
|
||||||
|
theme_config: themeSetting?.theme_config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 { FC } from 'react';
|
||||||
import { Form, Table, Dropdown } from 'react-bootstrap';
|
import { Form, Table, Dropdown, Button, Stack } 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';
|
||||||
|
|
||||||
|
@ -14,8 +14,14 @@ import {
|
||||||
Icon,
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import * as Type from '@/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
import { useChangeModal, useChangeUserRoleModal, useToast } from '@/hooks';
|
import {
|
||||||
import { useQueryUsers } from '@/services';
|
useUserModal,
|
||||||
|
useChangeModal,
|
||||||
|
useChangeUserRoleModal,
|
||||||
|
useChangePasswordModal,
|
||||||
|
useToast,
|
||||||
|
} from '@/hooks';
|
||||||
|
import { useQueryUsers, addUser, updateUserPassword } from '@/services';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { formatCount } from '@/utils';
|
import { formatCount } from '@/utils';
|
||||||
|
|
||||||
|
@ -66,11 +72,31 @@ const Users: FC = () => {
|
||||||
callback: refreshUsers,
|
callback: refreshUsers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userModal = useUserModal({
|
||||||
|
onConfirm: (userModel) => {
|
||||||
|
addUser(userModel).then(() => {
|
||||||
|
if (/all|staff/.test(curFilter) && curPage === 1) {
|
||||||
|
refreshUsers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const changePasswordModal = useChangePasswordModal({
|
||||||
|
onConfirm: (rd) => {
|
||||||
|
updateUserPassword(rd).then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update_password', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleAction = (type, user) => {
|
const handleAction = (type, user) => {
|
||||||
const { user_id, status, role_id, username } = user;
|
const { user_id, status, role_id, username } = user;
|
||||||
if (username === currentUser.username) {
|
if (username === currentUser.username) {
|
||||||
Toast.onShow({
|
Toast.onShow({
|
||||||
msg: t('fobidden_operate_self', { keyPrefix: 'toast' }),
|
msg: t('forbidden_operate_self', { keyPrefix: 'toast' }),
|
||||||
variant: 'warning',
|
variant: 'warning',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -88,6 +114,9 @@ const Users: FC = () => {
|
||||||
role_id,
|
role_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (type === 'password') {
|
||||||
|
changePasswordModal.onShow(user_id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilter = (e) => {
|
const handleFilter = (e) => {
|
||||||
|
@ -99,12 +128,20 @@ const Users: FC = () => {
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('title')}</h3>
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<Stack direction="horizontal" gap={3}>
|
||||||
<QueryGroup
|
<QueryGroup
|
||||||
data={UserFilterKeys}
|
data={UserFilterKeys}
|
||||||
currentSort={curFilter}
|
currentSort={curFilter}
|
||||||
sortKey="filter"
|
sortKey="filter"
|
||||||
i18nKeyPrefix="admin.users"
|
i18nKeyPrefix="admin.users"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline-primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => userModal.onShow()}>
|
||||||
|
{t('add_user')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Form.Control
|
<Form.Control
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -188,6 +225,10 @@ const Users: FC = () => {
|
||||||
<Icon name="three-dots-vertical" />
|
<Icon name="three-dots-vertical" />
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={() => handleAction('password', user)}>
|
||||||
|
{t('set_new_password')}
|
||||||
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => handleAction('status', user)}>
|
onClick={() => handleAction('status', user)}>
|
||||||
{t('change_status')}
|
{t('change_status')}
|
||||||
|
|
|
@ -17,6 +17,8 @@ const formPaths = [
|
||||||
'legal',
|
'legal',
|
||||||
'write',
|
'write',
|
||||||
'seo',
|
'seo',
|
||||||
|
'themes',
|
||||||
|
'css-html',
|
||||||
];
|
];
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
|
|
|
@ -237,6 +237,14 @@ const routes: RouteNode[] = [
|
||||||
path: 'flags',
|
path: 'flags',
|
||||||
page: 'pages/Admin/Flags',
|
page: 'pages/Admin/Flags',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'themes',
|
||||||
|
page: 'pages/Admin/Themes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'css-html',
|
||||||
|
page: 'pages/Admin/CssAndHtml',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'general',
|
path: 'general',
|
||||||
page: 'pages/Admin/General',
|
page: 'pages/Admin/General',
|
||||||
|
@ -277,6 +285,10 @@ const routes: RouteNode[] = [
|
||||||
path: 'seo',
|
path: 'seo',
|
||||||
page: 'pages/Admin/Seo',
|
page: 'pages/Admin/Seo',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
page: 'pages/Admin/Login',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// for review
|
// for review
|
||||||
|
|
|
@ -109,3 +109,33 @@ export const getSeoSetting = () => {
|
||||||
export const putSeoSetting = (params: Type.AdminSettingsSeo) => {
|
export const putSeoSetting = (params: Type.AdminSettingsSeo) => {
|
||||||
return request.put('/answer/admin/api/siteinfo/seo', params);
|
return request.put('/answer/admin/api/siteinfo/seo', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getThemeSetting = () => {
|
||||||
|
return request.get<Type.AdminSettingsTheme>(
|
||||||
|
'/answer/admin/api/siteinfo/theme',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putThemeSetting = (params: Type.AdminSettingsTheme) => {
|
||||||
|
return request.put('/answer/admin/api/siteinfo/theme', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPageCustom = () => {
|
||||||
|
return request.get<Type.AdminSettingsCustom>(
|
||||||
|
'/answer/admin/api/siteinfo/custom-css-html',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putPageCustom = (params: Type.AdminSettingsCustom) => {
|
||||||
|
return request.put('/answer/admin/api/siteinfo/custom-css-html', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLoginSetting = () => {
|
||||||
|
return request.get<Type.AdminSettingsLogin>(
|
||||||
|
'/answer/admin/api/siteinfo/login',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putLoginSetting = (params: Type.AdminSettingsLogin) => {
|
||||||
|
return request.put('/answer/admin/api/siteinfo/login', params);
|
||||||
|
};
|
||||||
|
|
|
@ -29,3 +29,18 @@ export const getUserRoles = () => {
|
||||||
export const changeUserRole = (params) => {
|
export const changeUserRole = (params) => {
|
||||||
return request.put('/answer/admin/api/user/role', params);
|
return request.put('/answer/admin/api/user/role', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addUser = (params: {
|
||||||
|
display_name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}) => {
|
||||||
|
return request.post('/answer/admin/api/user', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserPassword = (params: {
|
||||||
|
password: string;
|
||||||
|
user_id: string;
|
||||||
|
}) => {
|
||||||
|
return request.put('/answer/admin/api/user/password', params);
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue