mirror of https://gitee.com/answerdev/answer.git
Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center
This commit is contained in:
commit
4c4381e140
|
@ -1164,8 +1164,9 @@ ui:
|
||||||
seo: SEO
|
seo: SEO
|
||||||
customize: Customize
|
customize: Customize
|
||||||
themes: Themes
|
themes: Themes
|
||||||
css-html: CSS/HTML
|
css_html: CSS/HTML
|
||||||
login: Login
|
login: Login
|
||||||
|
privileges: Privileges
|
||||||
plugins: Plugins
|
plugins: Plugins
|
||||||
installed_plugins: Installed Plugins
|
installed_plugins: Installed Plugins
|
||||||
website_welcome: Welcome to {{site_name}}
|
website_welcome: Welcome to {{site_name}}
|
||||||
|
@ -1368,9 +1369,6 @@ ui:
|
||||||
label: Timezone
|
label: Timezone
|
||||||
msg: Timezone cannot be empty.
|
msg: Timezone cannot be empty.
|
||||||
text: Choose a city in the same timezone as you.
|
text: Choose a city in the same timezone as you.
|
||||||
avatar:
|
|
||||||
label: Default Avatar
|
|
||||||
text: For users without a custom avatar of their own.
|
|
||||||
smtp:
|
smtp:
|
||||||
page_title: SMTP
|
page_title: SMTP
|
||||||
from_email:
|
from_email:
|
||||||
|
@ -1514,7 +1512,30 @@ ui:
|
||||||
deactivate: Deactivate
|
deactivate: Deactivate
|
||||||
activate: Activate
|
activate: Activate
|
||||||
settings: Settings
|
settings: Settings
|
||||||
|
settings_users:
|
||||||
|
title: Users
|
||||||
|
avatar:
|
||||||
|
label: Default Avatar
|
||||||
|
text: For users without a custom avatar of their own.
|
||||||
|
profile_editable:
|
||||||
|
title: Profile Editable
|
||||||
|
allow_update_display_name:
|
||||||
|
label: Allow users to change their display name
|
||||||
|
allow_update_username:
|
||||||
|
label: Allow users to change their username
|
||||||
|
allow_update_avatar:
|
||||||
|
label: Allow users to change their profile image
|
||||||
|
allow_update_bio:
|
||||||
|
label: Allow users to change their about me
|
||||||
|
allow_update_website:
|
||||||
|
label: Allow users to change their website
|
||||||
|
allow_update_location:
|
||||||
|
label: Allow users to change their location
|
||||||
|
privilege:
|
||||||
|
title: Privileges
|
||||||
|
level:
|
||||||
|
label: Reputation required level
|
||||||
|
text: Choose the reputation required for the privileges
|
||||||
|
|
||||||
form:
|
form:
|
||||||
optional: (optional)
|
optional: (optional)
|
||||||
|
|
|
@ -36,6 +36,7 @@ module.exports = {
|
||||||
'react/no-unescaped-entities': 'off',
|
'react/no-unescaped-entities': 'off',
|
||||||
'react/require-default-props': 'off',
|
'react/require-default-props': 'off',
|
||||||
'arrow-body-style': 'off',
|
'arrow-body-style': 'off',
|
||||||
|
"global-require": "off",
|
||||||
'react/prop-types': 0,
|
'react/prop-types': 0,
|
||||||
'react/no-danger': 'off',
|
'react/no-danger': 'off',
|
||||||
'jsx-a11y/no-static-element-interactions': 'off',
|
'jsx-a11y/no-static-element-interactions': 'off',
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -74,7 +74,8 @@ export const ADMIN_NAV_MENUS = [
|
||||||
name: 'themes',
|
name: 'themes',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'css-html',
|
name: 'css_html',
|
||||||
|
path: 'css-html',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -89,6 +90,8 @@ export const ADMIN_NAV_MENUS = [
|
||||||
{ name: 'write' },
|
{ name: 'write' },
|
||||||
{ name: 'seo' },
|
{ name: 'seo' },
|
||||||
{ name: 'login' },
|
{ name: 'login' },
|
||||||
|
{ name: 'users', path: 'settings-users' },
|
||||||
|
{ name: 'privileges' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -96,6 +99,7 @@ export const ADMIN_NAV_MENUS = [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'installed_plugins',
|
name: 'installed_plugins',
|
||||||
|
path: 'installed-plugins',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -312,7 +312,6 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
|
||||||
export interface AdminSettingsInterface {
|
export interface AdminSettingsInterface {
|
||||||
language: string;
|
language: string;
|
||||||
time_zone?: string;
|
time_zone?: string;
|
||||||
default_avatar?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminSettingsSmtp {
|
export interface AdminSettingsSmtp {
|
||||||
|
|
|
@ -18,12 +18,12 @@ function MenuNode({
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||||
const isLeaf = !menu.children.length;
|
const isLeaf = !menu.children.length;
|
||||||
const href = isLeaf ? `${path}${menu.name}` : '#';
|
const href = isLeaf ? `${path}${menu.path}` : '#';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Nav.Item key={menu.name} className="w-100">
|
<Nav.Item key={menu.path} className="w-100">
|
||||||
<Nav.Link
|
<Nav.Link
|
||||||
eventKey={menu.name}
|
eventKey={menu.path}
|
||||||
as={isLeaf ? 'a' : 'button'}
|
as={isLeaf ? 'a' : 'button'}
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
callback(evt, menu, href, isLeaf);
|
callback(evt, menu, href, isLeaf);
|
||||||
|
@ -31,7 +31,7 @@ function MenuNode({
|
||||||
href={href}
|
href={href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'text-nowrap d-flex flex-nowrap align-items-center w-100',
|
'text-nowrap d-flex flex-nowrap align-items-center w-100',
|
||||||
{ expanding, 'link-dark': activeKey !== menu.name },
|
{ expanding, 'link-dark': activeKey !== menu.path },
|
||||||
)}>
|
)}>
|
||||||
<span className="me-auto text-truncate">
|
<span className="me-auto text-truncate">
|
||||||
{menu.displayName ? menu.displayName : t(menu.name)}
|
{menu.displayName ? menu.displayName : t(menu.name)}
|
||||||
|
@ -44,7 +44,7 @@ function MenuNode({
|
||||||
)}
|
)}
|
||||||
</Nav.Link>
|
</Nav.Link>
|
||||||
{menu.children.length ? (
|
{menu.children.length ? (
|
||||||
<Accordion.Collapse eventKey={menu.name} className="ms-3">
|
<Accordion.Collapse eventKey={menu.path} className="ms-3">
|
||||||
<>
|
<>
|
||||||
{menu.children.map((leaf) => {
|
{menu.children.map((leaf) => {
|
||||||
return (
|
return (
|
||||||
|
@ -53,7 +53,7 @@ function MenuNode({
|
||||||
callback={callback}
|
callback={callback}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
path={path}
|
path={path}
|
||||||
key={leaf.name}
|
key={leaf.path}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -73,17 +73,24 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
||||||
const pathMatch = useMatch(`${path}*`);
|
const pathMatch = useMatch(`${path}*`);
|
||||||
// auto set menu fields
|
// auto set menu fields
|
||||||
menus.forEach((m) => {
|
menus.forEach((m) => {
|
||||||
|
if (!m.path) {
|
||||||
|
m.path = m.name;
|
||||||
|
}
|
||||||
if (!Array.isArray(m.children)) {
|
if (!Array.isArray(m.children)) {
|
||||||
m.children = [];
|
m.children = [];
|
||||||
}
|
}
|
||||||
m.children.forEach((sm) => {
|
m.children.forEach((sm) => {
|
||||||
|
if (!sm.path) {
|
||||||
|
sm.path = sm.name;
|
||||||
|
}
|
||||||
if (!Array.isArray(sm.children)) {
|
if (!Array.isArray(sm.children)) {
|
||||||
sm.children = [];
|
sm.children = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const splat = pathMatch && pathMatch.params['*'];
|
const splat = pathMatch && pathMatch.params['*'];
|
||||||
let activeKey = menus[0].name;
|
let activeKey = menus[0].path;
|
||||||
if (splat) {
|
if (splat) {
|
||||||
activeKey = splat;
|
activeKey = splat;
|
||||||
}
|
}
|
||||||
|
@ -92,10 +99,10 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
||||||
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.path === activeKey;
|
||||||
});
|
});
|
||||||
if (matchedChild) {
|
if (matchedChild) {
|
||||||
openKey = li.name;
|
openKey = li.path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -111,7 +118,7 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
||||||
navigate(href);
|
navigate(href);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setOpenKey(openKey === menu.name ? '' : menu.name);
|
setOpenKey(openKey === menu.path ? '' : menu.path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -127,8 +134,8 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
||||||
path={path}
|
path={path}
|
||||||
callback={menuClick}
|
callback={menuClick}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
expanding={openKey === li.name}
|
expanding={openKey === li.path}
|
||||||
key={li.name}
|
key={li.path}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { usePageTags } from '@/hooks';
|
import { usePageTags } from '@/hooks';
|
||||||
|
|
||||||
const Index = ({ httpCode = '', errMsg = '' }) => {
|
const Index = ({ httpCode = '', errMsg = '', showErroCode = true }) => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_error' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_error' });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// auto height of container
|
// auto height of container
|
||||||
|
@ -31,7 +31,9 @@ const Index = ({ httpCode = '', errMsg = '' }) => {
|
||||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||||
(=‘x‘=)
|
(=‘x‘=)
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
|
{showErroCode && (
|
||||||
|
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
|
||||||
|
)}
|
||||||
<div className="text-center mb-3 fs-5">
|
<div className="text-center mb-3 fs-5">
|
||||||
{errMsg || t(`desc_${httpCode}`)}
|
{errMsg || t(`desc_${httpCode}`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Form, Stack } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: 'radio' | 'checkbox';
|
||||||
|
fieldName: string;
|
||||||
|
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
|
||||||
|
enumValues: (string | boolean | number)[];
|
||||||
|
enumNames: string[];
|
||||||
|
formData: Type.FormDataType;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({
|
||||||
|
type = 'radio',
|
||||||
|
fieldName,
|
||||||
|
onChange,
|
||||||
|
enumValues,
|
||||||
|
enumNames,
|
||||||
|
formData,
|
||||||
|
}) => {
|
||||||
|
const fieldObject = formData[fieldName];
|
||||||
|
return (
|
||||||
|
<Stack direction="horizontal">
|
||||||
|
{enumValues?.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Form.Check
|
||||||
|
key={String(item)}
|
||||||
|
inline
|
||||||
|
required
|
||||||
|
type={type}
|
||||||
|
name={fieldName}
|
||||||
|
id={`form-${String(item)}`}
|
||||||
|
label={enumNames?.[index]}
|
||||||
|
checked={(fieldObject?.value || '') === item}
|
||||||
|
feedback={fieldObject?.errorMsg}
|
||||||
|
feedbackType="invalid"
|
||||||
|
isInvalid={fieldObject?.isInvalid}
|
||||||
|
onChange={(evt) => onChange(evt, index)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: string | undefined;
|
||||||
|
placeholder: string | undefined;
|
||||||
|
fieldName: string;
|
||||||
|
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
|
||||||
|
formData: Type.FormDataType;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({
|
||||||
|
type = 'text',
|
||||||
|
placeholder = '',
|
||||||
|
fieldName,
|
||||||
|
onChange,
|
||||||
|
formData,
|
||||||
|
}) => {
|
||||||
|
const fieldObject = formData[fieldName];
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
name={fieldName}
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={type}
|
||||||
|
value={fieldObject?.value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
style={type === 'color' ? { width: '6rem' } : {}}
|
||||||
|
isInvalid={fieldObject?.isInvalid}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({ title }) => {
|
||||||
|
return <Form.Label>{title}</Form.Label>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
desc: string | undefined;
|
||||||
|
fieldName: string;
|
||||||
|
onChange: (evt: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||||
|
enumValues: (string | boolean | number)[];
|
||||||
|
enumNames: string[];
|
||||||
|
formData: Type.FormDataType;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({
|
||||||
|
desc,
|
||||||
|
fieldName,
|
||||||
|
onChange,
|
||||||
|
enumValues,
|
||||||
|
enumNames,
|
||||||
|
formData,
|
||||||
|
}) => {
|
||||||
|
const fieldObject = formData[fieldName];
|
||||||
|
return (
|
||||||
|
<Form.Select
|
||||||
|
aria-label={desc}
|
||||||
|
name={fieldName}
|
||||||
|
value={fieldObject?.value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
isInvalid={fieldObject?.isInvalid}>
|
||||||
|
{enumValues?.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<option value={String(item)} key={String(item)}>
|
||||||
|
{enumNames?.[index]}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form.Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
label: string | undefined;
|
||||||
|
fieldName: string;
|
||||||
|
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
|
||||||
|
formData: Type.FormDataType;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({ title, fieldName, onChange, label, formData }) => {
|
||||||
|
const fieldObject = formData[fieldName];
|
||||||
|
return (
|
||||||
|
<Form.Check
|
||||||
|
required
|
||||||
|
id={`switch-${title}`}
|
||||||
|
name={fieldName}
|
||||||
|
type="switch"
|
||||||
|
label={label}
|
||||||
|
checked={fieldObject?.value || ''}
|
||||||
|
feedback={fieldObject?.errorMsg}
|
||||||
|
feedbackType="invalid"
|
||||||
|
isInvalid={fieldObject.isInvalid}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
placeholder: string | undefined;
|
||||||
|
rows: number | undefined;
|
||||||
|
className: classnames.Argument;
|
||||||
|
fieldName: string;
|
||||||
|
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
|
||||||
|
formData: Type.FormDataType;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({
|
||||||
|
placeholder = '',
|
||||||
|
rows = 3,
|
||||||
|
className,
|
||||||
|
fieldName,
|
||||||
|
onChange,
|
||||||
|
formData,
|
||||||
|
}) => {
|
||||||
|
const fieldObject = formData[fieldName];
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
name={fieldName}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={fieldObject?.value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
isInvalid={fieldObject?.isInvalid}
|
||||||
|
rows={rows}
|
||||||
|
className={classnames(className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import TimeZonePicker from '@/components/TimeZonePicker';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fieldName: string;
|
||||||
|
onChange: (evt: React.ChangeEvent<HTMLSelectElement>, ...rest) => void;
|
||||||
|
formData: Type.FormDataType;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({ fieldName, onChange, formData }) => {
|
||||||
|
const fieldObject = formData[fieldName];
|
||||||
|
return (
|
||||||
|
<TimeZonePicker
|
||||||
|
value={fieldObject?.value || ''}
|
||||||
|
isInvalid={fieldObject?.isInvalid}
|
||||||
|
name={fieldName}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import BrandUpload from '@/components/BrandUpload';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: Type.UploadType | undefined;
|
||||||
|
acceptType: string | undefined;
|
||||||
|
fieldName: string;
|
||||||
|
onChange: (key, val) => void;
|
||||||
|
formData: Type.FormDataType;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({
|
||||||
|
type = 'avatar',
|
||||||
|
acceptType = '',
|
||||||
|
fieldName,
|
||||||
|
onChange,
|
||||||
|
formData,
|
||||||
|
}) => {
|
||||||
|
const fieldObject = formData[fieldName];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BrandUpload
|
||||||
|
type={type}
|
||||||
|
acceptType={acceptType}
|
||||||
|
value={fieldObject?.value}
|
||||||
|
onChange={(value) => onChange(fieldName, value)}
|
||||||
|
/>
|
||||||
|
<Form.Control
|
||||||
|
name={fieldName}
|
||||||
|
className="d-none"
|
||||||
|
isInvalid={fieldObject?.isInvalid}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Legend from './Legend';
|
||||||
|
import Select from './Select';
|
||||||
|
import Check from './Check';
|
||||||
|
import Switch from './Switch';
|
||||||
|
import Timezone from './Timezone';
|
||||||
|
import Upload from './Upload';
|
||||||
|
import Textarea from './Textarea';
|
||||||
|
import Input from './Input';
|
||||||
|
|
||||||
|
export { Legend, Select, Check, Switch, Timezone, Upload, Textarea, Input };
|
|
@ -1,18 +1,27 @@
|
||||||
import {
|
import React, {
|
||||||
ForwardRefRenderFunction,
|
ForwardRefRenderFunction,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useEffect,
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Form, Button, Stack } from 'react-bootstrap';
|
import { Form, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import BrandUpload from '../BrandUpload';
|
|
||||||
import TimeZonePicker from '../TimeZonePicker';
|
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Legend,
|
||||||
|
Select,
|
||||||
|
Check,
|
||||||
|
Switch,
|
||||||
|
Timezone,
|
||||||
|
Upload,
|
||||||
|
Textarea,
|
||||||
|
Input,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
export interface JSONSchema {
|
export interface JSONSchema {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -31,7 +40,12 @@ export interface JSONSchema {
|
||||||
|
|
||||||
export interface BaseUIOptions {
|
export interface BaseUIOptions {
|
||||||
empty?: string;
|
empty?: string;
|
||||||
className?: string | string[];
|
// Will be appended to the className of the form component itself
|
||||||
|
className?: classnames.Argument;
|
||||||
|
// The className that will be attached to a form field container
|
||||||
|
fieldClassName?: classnames.Argument;
|
||||||
|
// Make a form component render into simplified mode
|
||||||
|
simplify?: boolean;
|
||||||
validator?: (
|
validator?: (
|
||||||
value,
|
value,
|
||||||
formData?,
|
formData?,
|
||||||
|
@ -96,7 +110,8 @@ export type UIWidget =
|
||||||
| 'select'
|
| 'select'
|
||||||
| 'upload'
|
| 'upload'
|
||||||
| 'timezone'
|
| 'timezone'
|
||||||
| 'switch';
|
| 'switch'
|
||||||
|
| 'legend';
|
||||||
export interface UISchema {
|
export interface UISchema {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
'ui:widget'?: UIWidget;
|
'ui:widget'?: UIWidget;
|
||||||
|
@ -117,6 +132,13 @@ interface IRef {
|
||||||
validator: () => Promise<boolean>;
|
validator: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO:
|
||||||
|
* * Normalize and document `formData[key].hidden && 'd-none'`
|
||||||
|
* * `handleXXChange` methods are placed in the concrete component
|
||||||
|
* * Improving field hints for `formData`
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* json schema form
|
* json schema form
|
||||||
* @param schema json schema
|
* @param schema json schema
|
||||||
|
@ -139,9 +161,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'form',
|
keyPrefix: 'form',
|
||||||
});
|
});
|
||||||
|
const { required = [], properties = {} } = schema || {};
|
||||||
const { required = [], properties } = schema;
|
|
||||||
|
|
||||||
// check required field
|
// check required field
|
||||||
const excludes = required.filter((key) => !properties[key]);
|
const excludes = required.filter((key) => !properties[key]);
|
||||||
|
|
||||||
|
@ -174,7 +194,6 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDefaultValueAsDomBehaviour();
|
setDefaultValueAsDomBehaviour();
|
||||||
}, [formData]);
|
}, [formData]);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
const data = {
|
const data = {
|
||||||
|
@ -345,219 +364,123 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
validator,
|
validator,
|
||||||
}));
|
}));
|
||||||
|
if (!formData || !schema || !schema.properties) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
{keys.map((key) => {
|
{keys.map((key) => {
|
||||||
const { title, description } = properties[key];
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
enum: enumValues = [],
|
||||||
|
enumNames = [],
|
||||||
|
} = properties[key];
|
||||||
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
|
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
|
||||||
uiSchema[key] || {};
|
uiSchema[key] || {};
|
||||||
if (widget === 'select') {
|
const fieldData = formData[key];
|
||||||
return (
|
const uiSimplify = widget === 'legend' || uiOpt?.simplify;
|
||||||
<Form.Group
|
let groupClassName: BaseUIOptions['fieldClassName'] = uiOpt?.simplify
|
||||||
key={title}
|
? 'mb-2'
|
||||||
controlId={key}
|
: 'mb-3';
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
if (widget === 'legend') {
|
||||||
<Form.Label>{title}</Form.Label>
|
groupClassName = 'mb-0';
|
||||||
<Form.Select
|
|
||||||
aria-label={description}
|
|
||||||
name={key}
|
|
||||||
value={formData[key]?.value || ''}
|
|
||||||
onChange={handleSelectChange}
|
|
||||||
isInvalid={formData[key].isInvalid}>
|
|
||||||
{properties[key].enum?.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<option value={String(item)} key={String(item)}>
|
|
||||||
{properties[key].enumNames?.[index]}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Form.Select>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData[key]?.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
{description && (
|
|
||||||
<Form.Text className="text-muted">{description}</Form.Text>
|
|
||||||
)}
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (uiOpt?.fieldClassName) {
|
||||||
if (widget === 'checkbox' || widget === 'radio') {
|
groupClassName = uiOpt.fieldClassName;
|
||||||
return (
|
|
||||||
<Form.Group
|
|
||||||
key={title}
|
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
|
||||||
controlId={key}>
|
|
||||||
<Form.Label>{title}</Form.Label>
|
|
||||||
<Stack direction="horizontal">
|
|
||||||
{properties[key].enum?.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<Form.Check
|
|
||||||
key={String(item)}
|
|
||||||
inline
|
|
||||||
required
|
|
||||||
type={widget}
|
|
||||||
name={key}
|
|
||||||
id={`form-${String(item)}`}
|
|
||||||
label={properties[key].enumNames?.[index]}
|
|
||||||
checked={(formData[key]?.value || '') === item}
|
|
||||||
feedback={formData[key]?.errorMsg}
|
|
||||||
feedbackType="invalid"
|
|
||||||
isInvalid={formData[key].isInvalid}
|
|
||||||
onChange={(e) => handleInputCheck(e, index)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData[key]?.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
{description && (
|
|
||||||
<Form.Text className="text-muted">{description}</Form.Text>
|
|
||||||
)}
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget === 'switch') {
|
|
||||||
return (
|
|
||||||
<Form.Group
|
|
||||||
key={title}
|
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
|
||||||
controlId={key}>
|
|
||||||
<Form.Label>{title}</Form.Label>
|
|
||||||
<Form.Check
|
|
||||||
required
|
|
||||||
id={`switch-${title}`}
|
|
||||||
name={key}
|
|
||||||
type="switch"
|
|
||||||
label={(uiOpt as SwitchOptions)?.label}
|
|
||||||
checked={formData[key]?.value || ''}
|
|
||||||
feedback={formData[key]?.errorMsg}
|
|
||||||
feedbackType="invalid"
|
|
||||||
isInvalid={formData[key].isInvalid}
|
|
||||||
onChange={handleSwitchChange}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData[key]?.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
{description && (
|
|
||||||
<Form.Text className="text-muted">{description}</Form.Text>
|
|
||||||
)}
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (widget === 'timezone') {
|
|
||||||
return (
|
|
||||||
<Form.Group
|
|
||||||
key={title}
|
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
|
||||||
controlId={key}>
|
|
||||||
<Form.Label>{title}</Form.Label>
|
|
||||||
<TimeZonePicker
|
|
||||||
value={formData[key]?.value || ''}
|
|
||||||
name={key}
|
|
||||||
onChange={handleSelectChange}
|
|
||||||
/>
|
|
||||||
<Form.Control
|
|
||||||
name={key}
|
|
||||||
className="d-none"
|
|
||||||
isInvalid={formData[key].isInvalid}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData[key]?.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
{description && (
|
|
||||||
<Form.Text className="text-muted">{description}</Form.Text>
|
|
||||||
)}
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget === 'upload') {
|
|
||||||
const options: UploadOptions = uiSchema[key]?.['ui:options'] || {};
|
|
||||||
return (
|
|
||||||
<Form.Group
|
|
||||||
key={title}
|
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
|
||||||
controlId={key}>
|
|
||||||
<Form.Label>{title}</Form.Label>
|
|
||||||
<BrandUpload
|
|
||||||
type={options.imageType || 'avatar'}
|
|
||||||
acceptType={options.acceptType || ''}
|
|
||||||
value={formData[key]?.value}
|
|
||||||
onChange={(value) => handleUploadChange(key, value)}
|
|
||||||
/>
|
|
||||||
<Form.Control
|
|
||||||
name={key}
|
|
||||||
className="d-none"
|
|
||||||
isInvalid={formData[key].isInvalid}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData[key]?.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
{description && (
|
|
||||||
<Form.Text className="text-muted">{description}</Form.Text>
|
|
||||||
)}
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget === 'textarea') {
|
|
||||||
const options: TextareaOptions = uiSchema[key]?.['ui:options'] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Group
|
|
||||||
controlId={`form-${key}`}
|
|
||||||
key={key}
|
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
|
||||||
<Form.Label>{title}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
name={key}
|
|
||||||
placeholder={options?.placeholder || ''}
|
|
||||||
value={formData[key]?.value || ''}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
isInvalid={formData[key].isInvalid}
|
|
||||||
rows={options?.rows || 3}
|
|
||||||
className={classnames(options.className)}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData[key]?.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
|
|
||||||
{description && (
|
|
||||||
<Form.Text className="text-muted">{description}</Form.Text>
|
|
||||||
)}
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: InputOptions = uiSchema[key]?.['ui:options'] || {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group
|
<Form.Group
|
||||||
|
key={title}
|
||||||
controlId={key}
|
controlId={key}
|
||||||
key={key}
|
className={classnames(
|
||||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
groupClassName,
|
||||||
<Form.Label>{title}</Form.Label>
|
formData[key].hidden ? 'd-none' : null,
|
||||||
<Form.Control
|
)}>
|
||||||
name={key}
|
{/* Uniform processing `label` */}
|
||||||
placeholder={options?.placeholder || ''}
|
{title && !uiSimplify ? <Form.Label>{title}</Form.Label> : null}
|
||||||
type={options?.inputType || 'text'}
|
{/* Handling of individual specific controls */}
|
||||||
value={formData[key]?.value || ''}
|
{widget === 'legend' ? <Legend title={title} /> : null}
|
||||||
onChange={handleInputChange}
|
{widget === 'select' ? (
|
||||||
style={options?.inputType === 'color' ? { width: '6rem' } : {}}
|
<Select
|
||||||
isInvalid={formData[key].isInvalid}
|
desc={description}
|
||||||
/>
|
fieldName={key}
|
||||||
|
onChange={handleSelectChange}
|
||||||
|
enumValues={enumValues}
|
||||||
|
enumNames={enumNames}
|
||||||
|
formData={formData}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{widget === 'radio' || widget === 'checkbox' ? (
|
||||||
|
<Check
|
||||||
|
type={widget}
|
||||||
|
fieldName={key}
|
||||||
|
onChange={handleInputCheck}
|
||||||
|
enumValues={enumValues}
|
||||||
|
enumNames={enumNames}
|
||||||
|
formData={formData}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{widget === 'switch' ? (
|
||||||
|
<Switch
|
||||||
|
title={title}
|
||||||
|
label={uiOpt && 'label' in uiOpt ? uiOpt.label : ''}
|
||||||
|
fieldName={key}
|
||||||
|
onChange={handleSwitchChange}
|
||||||
|
formData={formData}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{widget === 'timezone' ? (
|
||||||
|
<Timezone
|
||||||
|
fieldName={key}
|
||||||
|
onChange={handleSelectChange}
|
||||||
|
formData={formData}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{widget === 'upload' ? (
|
||||||
|
<Upload
|
||||||
|
type={
|
||||||
|
uiOpt && 'imageType' in uiOpt ? uiOpt.imageType : undefined
|
||||||
|
}
|
||||||
|
acceptType={
|
||||||
|
uiOpt && 'acceptType' in uiOpt ? uiOpt.acceptType : ''
|
||||||
|
}
|
||||||
|
fieldName={key}
|
||||||
|
onChange={handleUploadChange}
|
||||||
|
formData={formData}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{widget === 'textarea' ? (
|
||||||
|
<Textarea
|
||||||
|
placeholder={
|
||||||
|
uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
|
||||||
|
}
|
||||||
|
rows={uiOpt && 'rows' in uiOpt ? uiOpt.rows : 3}
|
||||||
|
className={uiOpt && 'className' in uiOpt ? uiOpt.className : ''}
|
||||||
|
fieldName={key}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
formData={formData}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{widget === 'input' ? (
|
||||||
|
<Input
|
||||||
|
type={uiOpt && 'inputType' in uiOpt ? uiOpt.inputType : 'text'}
|
||||||
|
placeholder={
|
||||||
|
uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
|
||||||
|
}
|
||||||
|
fieldName={key}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
formData={formData}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{/* Unified handling of `Feedback` and `Text` */}
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData[key]?.errorMsg}
|
{fieldData?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
|
{description ? (
|
||||||
{description && (
|
|
||||||
<Form.Text className="text-muted">{description}</Form.Text>
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
)}
|
) : null}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { HttpErrorContent } from '@/components';
|
import { HttpErrorContent } from '@/components';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
return <HttpErrorContent httpCode="50X" />;
|
const [searchParams] = useSearchParams();
|
||||||
|
const msg = searchParams.get('msg') || '';
|
||||||
|
return <HttpErrorContent httpCode="50X" errMsg={msg} showErroCode={!msg} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Index;
|
export default Index;
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from '@/common/interface';
|
} from '@/common/interface';
|
||||||
import { interfaceStore, loggedUserInfoStore } from '@/stores';
|
import { interfaceStore, loggedUserInfoStore } from '@/stores';
|
||||||
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
||||||
import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
|
import { DEFAULT_TIMEZONE } from '@/common/constants';
|
||||||
import {
|
import {
|
||||||
updateInterfaceSetting,
|
updateInterfaceSetting,
|
||||||
useInterfaceSetting,
|
useInterfaceSetting,
|
||||||
|
@ -48,13 +48,6 @@ const Interface: FC = () => {
|
||||||
description: t('time_zone.text'),
|
description: t('time_zone.text'),
|
||||||
default: setting?.time_zone || DEFAULT_TIMEZONE,
|
default: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||||
},
|
},
|
||||||
default_avatar: {
|
|
||||||
type: 'string',
|
|
||||||
title: t('avatar.label'),
|
|
||||||
description: t('avatar.text'),
|
|
||||||
enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),
|
|
||||||
enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,11 +62,6 @@ const Interface: FC = () => {
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
default_avatar: {
|
|
||||||
value: setting?.default_avatar || 'system',
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
|
@ -83,9 +71,6 @@ const Interface: FC = () => {
|
||||||
time_zone: {
|
time_zone: {
|
||||||
'ui:widget': 'timezone',
|
'ui:widget': 'timezone',
|
||||||
},
|
},
|
||||||
default_avatar: {
|
|
||||||
'ui:widget': 'select',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const getLangs = async () => {
|
const getLangs = async () => {
|
||||||
const res: LangsType[] = await loadLanguageOptions(true);
|
const res: LangsType[] = await loadLanguageOptions(true);
|
||||||
|
@ -118,7 +103,6 @@ const Interface: FC = () => {
|
||||||
const reqParams: AdminSettingsInterface = {
|
const reqParams: AdminSettingsInterface = {
|
||||||
language: formData.language.value,
|
language: formData.language.value,
|
||||||
time_zone: formData.time_zone.value,
|
time_zone: formData.time_zone.value,
|
||||||
default_avatar: formData.default_avatar.value,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateInterfaceSetting(reqParams)
|
updateInterfaceSetting(reqParams)
|
||||||
|
@ -147,9 +131,6 @@ const Interface: FC = () => {
|
||||||
const formMeta = {};
|
const formMeta = {};
|
||||||
Object.keys(setting).forEach((k) => {
|
Object.keys(setting).forEach((k) => {
|
||||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||||
if (k === 'default_avatar') {
|
|
||||||
formMeta[k].value = setting[k] || 'system';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
setFormData({ ...formData, ...formMeta });
|
setFormData({ ...formData, ...formMeta });
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { FC, FormEvent, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useToast } from '@/hooks';
|
||||||
|
import { FormDataType } from '@/common/interface';
|
||||||
|
import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
|
||||||
|
import {
|
||||||
|
getPrivilegeSetting,
|
||||||
|
putPrivilegeSetting,
|
||||||
|
AdminSettingsPrivilege,
|
||||||
|
} from '@/services';
|
||||||
|
import { handleFormError } from '@/utils';
|
||||||
|
import * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'admin.privilege',
|
||||||
|
});
|
||||||
|
const Toast = useToast();
|
||||||
|
const [privilege, setPrivilege] = useState<AdminSettingsPrivilege>();
|
||||||
|
|
||||||
|
const schema: JSONSchema = {
|
||||||
|
title: t('title'),
|
||||||
|
properties: {
|
||||||
|
level: {
|
||||||
|
type: 'number',
|
||||||
|
title: t('level.label'),
|
||||||
|
description: t('level.text'),
|
||||||
|
enum: privilege?.options.map((_) => _.level),
|
||||||
|
enumNames: privilege?.options.map((_) => _.level_desc),
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||||
|
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
level: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (evt: FormEvent) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const lv = Number(formData.level.value);
|
||||||
|
putPrivilegeSetting(lv)
|
||||||
|
.then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData);
|
||||||
|
setFormData({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getPrivilegeSetting().then((resp) => {
|
||||||
|
setPrivilege(resp);
|
||||||
|
const formMeta: Type.FormDataType = {};
|
||||||
|
formMeta.level = {
|
||||||
|
value: resp.selected_level,
|
||||||
|
errorMsg: '',
|
||||||
|
isInvalid: false,
|
||||||
|
};
|
||||||
|
setFormData({ ...formData, ...formMeta });
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
formData={formData}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { FC, FormEvent, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useToast } from '@/hooks';
|
||||||
|
import { FormDataType } from '@/common/interface';
|
||||||
|
import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
|
||||||
|
import { SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
|
||||||
|
import {
|
||||||
|
getUsersSetting,
|
||||||
|
putUsersSetting,
|
||||||
|
AdminSettingsUsers,
|
||||||
|
} from '@/services';
|
||||||
|
import { handleFormError } from '@/utils';
|
||||||
|
import * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'admin.settings_users',
|
||||||
|
});
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const schema: JSONSchema = {
|
||||||
|
title: t('title'),
|
||||||
|
properties: {
|
||||||
|
default_avatar: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('avatar.label'),
|
||||||
|
description: t('avatar.text'),
|
||||||
|
enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),
|
||||||
|
enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),
|
||||||
|
default: 'system',
|
||||||
|
},
|
||||||
|
profile_editable: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('profile_editable.title'),
|
||||||
|
},
|
||||||
|
allow_update_display_name: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'allow_update_display_name',
|
||||||
|
},
|
||||||
|
allow_update_username: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'allow_update_username',
|
||||||
|
},
|
||||||
|
allow_update_avatar: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'allow_update_avatar',
|
||||||
|
},
|
||||||
|
allow_update_bio: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'allow_update_bio',
|
||||||
|
},
|
||||||
|
allow_update_website: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'allow_update_website',
|
||||||
|
},
|
||||||
|
allow_update_location: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'allow_update_location',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||||
|
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
default_avatar: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
|
profile_editable: {
|
||||||
|
'ui:widget': 'legend',
|
||||||
|
},
|
||||||
|
allow_update_display_name: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('allow_update_display_name.label'),
|
||||||
|
simplify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allow_update_username: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('allow_update_username.label'),
|
||||||
|
simplify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allow_update_avatar: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('allow_update_avatar.label'),
|
||||||
|
simplify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allow_update_bio: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('allow_update_bio.label'),
|
||||||
|
simplify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allow_update_website: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('allow_update_website.label'),
|
||||||
|
simplify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allow_update_location: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('allow_update_location.label'),
|
||||||
|
fieldClassName: 'mb-3',
|
||||||
|
simplify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (evt: FormEvent) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const reqParams: AdminSettingsUsers = {
|
||||||
|
allow_update_avatar: formData.allow_update_avatar.value,
|
||||||
|
allow_update_bio: formData.allow_update_bio.value,
|
||||||
|
allow_update_display_name: formData.allow_update_display_name.value,
|
||||||
|
allow_update_location: formData.allow_update_location.value,
|
||||||
|
allow_update_username: formData.allow_update_username.value,
|
||||||
|
allow_update_website: formData.allow_update_website.value,
|
||||||
|
default_avatar: formData.default_avatar.value,
|
||||||
|
};
|
||||||
|
putUsersSetting(reqParams)
|
||||||
|
.then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData);
|
||||||
|
setFormData({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUsersSetting().then((resp) => {
|
||||||
|
const formMeta: Type.FormDataType = {};
|
||||||
|
Object.keys(formData).forEach((k) => {
|
||||||
|
let v = resp[k];
|
||||||
|
if (k === 'default_avatar' && !v) {
|
||||||
|
v = 'system';
|
||||||
|
}
|
||||||
|
formMeta[k] = { ...formData[k], value: v };
|
||||||
|
});
|
||||||
|
setFormData({ ...formData, ...formMeta });
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
formData={formData}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -19,7 +19,7 @@ const g10Paths = [
|
||||||
'answers',
|
'answers',
|
||||||
'users',
|
'users',
|
||||||
'flags',
|
'flags',
|
||||||
'installed_plugins',
|
'installed-plugins',
|
||||||
];
|
];
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
|
@ -80,7 +80,7 @@ const Index: FC = () => {
|
||||||
<Col lg={2}>
|
<Col lg={2}>
|
||||||
<AccordionNav menus={menus} path="/admin/" />
|
<AccordionNav menus={menus} path="/admin/" />
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={g10Paths.find((v) => curPath.includes(v)) ? 10 : 6}>
|
<Col lg={g10Paths.find((v) => curPath === v) ? 10 : 6}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Container, Card, Col, Carousel } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: require('@/assets/images/carousel-wecom-1.jpg'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
url: require('@/assets/images/carousel-wecom-2.jpg'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
url: require('@/assets/images/carousel-wecom-3.jpg'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
url: require('@/assets/images/carousel-wecom-4.jpg'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
url: require('@/assets/images/carousel-wecom-5.jpg'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Index = () => {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Col lg={4} className="mx-auto mt-3 py-5">
|
||||||
|
<Card>
|
||||||
|
<Card.Body>
|
||||||
|
<h3 className="text-center pt-3 mb-3">WeCome Login</h3>
|
||||||
|
<p className="text-danger text-center">
|
||||||
|
Login failed, please allow this app to access your email
|
||||||
|
information before try again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Carousel controls={false}>
|
||||||
|
{data.map((item) => (
|
||||||
|
<Carousel.Item key={item.id}>
|
||||||
|
<img
|
||||||
|
className="d-block w-100"
|
||||||
|
src={item.url}
|
||||||
|
alt="First slide"
|
||||||
|
/>
|
||||||
|
</Carousel.Item>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -345,7 +345,15 @@ const routes: RouteNode[] = [
|
||||||
page: 'pages/Admin/Login',
|
page: 'pages/Admin/Login',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'installed_plugins',
|
path: 'settings-users',
|
||||||
|
page: 'pages/Admin/SettingsUsers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'privileges',
|
||||||
|
page: 'pages/Admin/Privileges',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'installed-plugins',
|
||||||
page: 'pages/Admin/Plugins/Installed',
|
page: 'pages/Admin/Plugins/Installed',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -367,6 +375,10 @@ const routes: RouteNode[] = [
|
||||||
path: '50x',
|
path: '50x',
|
||||||
page: 'pages/50X',
|
page: 'pages/50X',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'login-fail',
|
||||||
|
page: 'pages/LoginFail',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,6 +3,29 @@ import useSWR from 'swr';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
export interface AdminSettingsUsers {
|
||||||
|
allow_update_avatar: boolean;
|
||||||
|
allow_update_bio: boolean;
|
||||||
|
allow_update_display_name: boolean;
|
||||||
|
allow_update_location: boolean;
|
||||||
|
allow_update_username: boolean;
|
||||||
|
allow_update_website: boolean;
|
||||||
|
default_avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PrivilegeLevel {
|
||||||
|
level: number;
|
||||||
|
level_desc: string;
|
||||||
|
privileges: {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
export interface AdminSettingsPrivilege {
|
||||||
|
selected_level: number;
|
||||||
|
options: PrivilegeLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
export const useGeneralSetting = () => {
|
export const useGeneralSetting = () => {
|
||||||
const apiUrl = `/answer/admin/api/siteinfo/general`;
|
const apiUrl = `/answer/admin/api/siteinfo/general`;
|
||||||
const { data, error } = useSWR<Type.AdminSettingsGeneral, Error>(
|
const { data, error } = useSWR<Type.AdminSettingsGeneral, Error>(
|
||||||
|
@ -126,3 +149,23 @@ export const getLoginSetting = () => {
|
||||||
export const putLoginSetting = (params: Type.AdminSettingsLogin) => {
|
export const putLoginSetting = (params: Type.AdminSettingsLogin) => {
|
||||||
return request.put('/answer/admin/api/siteinfo/login', params);
|
return request.put('/answer/admin/api/siteinfo/login', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUsersSetting = () => {
|
||||||
|
return request.get<AdminSettingsUsers>('/answer/admin/api/siteinfo/users');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putUsersSetting = (params: AdminSettingsUsers) => {
|
||||||
|
return request.put('/answer/admin/api/siteinfo/users', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPrivilegeSetting = () => {
|
||||||
|
return request.get<AdminSettingsPrivilege>(
|
||||||
|
'/answer/admin/api/setting/privileges',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putPrivilegeSetting = (level: number) => {
|
||||||
|
return request.put('/answer/admin/api/setting/privileges', {
|
||||||
|
level,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue