This is an automated email from the ASF dual-hosted git repository.
shuai pushed a commit to branch test
in repository https://gitbox.apache.org/repos/asf/answer.git
The following commit(s) were added to refs/heads/test by this push:
new 630ac20a Management Backend Menu and Function Adjustments (#1474)
630ac20a is described below
commit 630ac20a384cad4b4e1527528c2fbbc50b937933
Author: dashuai <[email protected]>
AuthorDate: Thu Jan 22 14:07:30 2026 +0800
Management Backend Menu and Function Adjustments (#1474)
…ent, regardless of functional split or reorganization
---
i18n/en_US.yaml | 9 +-
i18n/zh_CN.yaml | 9 +-
ui/src/common/constants.ts | 58 ++-
ui/src/common/interface.ts | 37 +-
ui/src/components/AccordionNav/index.tsx | 65 ++-
ui/src/components/AdminSideNav/index.tsx | 13 +-
ui/src/components/SchemaForm/components/Switch.tsx | 3 +-
ui/src/components/SchemaForm/index.tsx | 8 +-
ui/src/components/SchemaForm/types.ts | 2 +-
ui/src/components/TabNav/index.tsx | 26 ++
ui/src/components/index.ts | 2 +
ui/src/pages/Admin/Answers/index.tsx | 8 +-
ui/src/pages/Admin/Branding/index.tsx | 16 +-
ui/src/pages/Admin/CssAndHtml/index.tsx | 16 +-
.../Dashboard/components/HealthStatus/index.tsx | 6 +-
ui/src/pages/Admin/Files/index.tsx | 261 ++++++++++++
ui/src/pages/Admin/General/index.tsx | 29 +-
ui/src/pages/Admin/Interface/index.tsx | 75 +---
ui/src/pages/Admin/Login/index.tsx | 30 +-
ui/src/pages/Admin/Plugins/Config/index.tsx | 18 +-
ui/src/pages/Admin/{Legal => Policies}/index.tsx | 63 +--
ui/src/pages/Admin/Privileges/index.tsx | 32 +-
ui/src/pages/Admin/QaSettings/index.tsx | 132 ++++++
ui/src/pages/Admin/Questions/index.tsx | 4 +-
ui/src/pages/Admin/Security/index.tsx | 145 +++++++
ui/src/pages/Admin/Seo/index.tsx | 16 +-
ui/src/pages/Admin/Smtp/index.tsx | 16 +-
ui/src/pages/Admin/TagsSettings/index.tsx | 170 ++++++++
ui/src/pages/Admin/Themes/index.tsx | 16 +-
ui/src/pages/Admin/Users/index.tsx | 3 +
ui/src/pages/Admin/UsersSettings/index.tsx | 132 ++++++
ui/src/pages/Admin/Write/index.tsx | 473 ---------------------
ui/src/pages/Admin/index.scss | 4 +
ui/src/pages/Admin/index.tsx | 16 +-
ui/src/pages/Layout/index.tsx | 5 +-
ui/src/pages/Users/Settings/Profile/index.tsx | 10 +-
ui/src/router/routes.ts | 36 +-
ui/src/services/admin/index.ts | 1 +
ui/src/services/admin/question.ts | 10 +
ui/src/services/admin/settings.ts | 38 +-
.../Admin/index.scss => services/admin/tags.ts} | 23 +-
ui/src/services/admin/users.ts | 19 +
ui/src/stores/index.ts | 4 +-
ui/src/stores/interface.ts | 4 +-
ui/src/stores/loginSetting.ts | 1 -
ui/src/stores/siteInfo.ts | 3 +-
ui/src/stores/{siteLegal.ts => siteSecurity.ts} | 16 +-
ui/src/stores/writeSetting.ts | 12 +-
ui/src/utils/guard.ts | 15 +-
49 files changed, 1304 insertions(+), 806 deletions(-)
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 653793f2..ea607366 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1816,6 +1816,13 @@ ui:
plugins: Plugins
installed_plugins: Installed Plugins
apperance: Appearance
+ community: Community
+ advanced: Advanced
+ tags: Tags
+ rules: Rules
+ policies: Policies
+ security: Security
+ files: Files
website_welcome: Welcome to {{site_name}}
user_center:
login: Login
@@ -2128,7 +2135,7 @@ ui:
always_display: Always display external content
ask_before_display: Ask before displaying external content
write:
- page_title: Write
+ page_title: Files
min_content:
label: Minimum question body length
text: Minimum allowed question body length in characters.
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index a6c7f198..29e18151 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -1778,6 +1778,13 @@ ui:
plugins: 插件
installed_plugins: 已安装插件
apperance: 外观
+ community: 社区
+ advanced: 高级
+ tags: 标签
+ rules: 规则
+ policies: 政策
+ security: 安全
+ files: 文件
website_welcome: 欢迎来到 {{site_name}}
user_center:
login: 登录
@@ -2089,7 +2096,7 @@ ui:
always_display: 总是显示外部内容
ask_before_display: 在显示外部内容之前询问
write:
- page_title: 编辑
+ page_title: 文件
min_content:
label: 最小问题长度
text: 最小允许的问题内容长度(字符)。
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index 18f25114..285da474 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -83,6 +83,11 @@ export const ADMIN_LIST_STATUS = {
},
};
+/**
+ * ADMIN_NAV_MENUS is the navigation menu for the admin panel.
+ * pathPrefix is used to activate the menu item when the activeKey starts with
the pathPrefix.
+ */
+
export const ADMIN_NAV_MENUS = [
{
name: 'dashboard',
@@ -92,15 +97,19 @@ export const ADMIN_NAV_MENUS = [
{
name: 'contents',
icon: 'file-earmark-text-fill',
- children: [{ name: 'questions' }, { name: 'answers' }],
+ children: [
+ { name: 'questions', path: 'qa/questions', pathPrefix: 'qa/' },
+ { name: 'tags', path: 'tags/settings', pathPrefix: 'tags/' },
+ ],
},
{
- name: 'users',
+ name: 'community',
icon: 'people-fill',
- },
- {
- name: 'badges',
- icon: 'award-fill',
+ children: [
+ { name: 'users', pathPrefix: 'users/' },
+ { name: 'badges' },
+ { name: 'rules', path: 'rules/privileges', pathPrefix: 'rules/' },
+ ],
},
{
name: 'apperance',
@@ -113,20 +122,19 @@ export const ADMIN_NAV_MENUS = [
name: 'customize',
},
{ name: 'branding' },
+ { name: 'interface' },
],
},
{
- name: 'settings',
+ name: 'advanced',
icon: 'gear-fill',
children: [
{ name: 'general' },
- { name: 'interface' },
- { name: 'smtp' },
- { name: 'legal' },
- { name: 'write' },
- { name: 'seo' },
+ { name: 'security' },
+ { name: 'files' },
{ name: 'login' },
- { name: 'privileges' },
+ { name: 'seo' },
+ { name: 'smtp' },
],
},
{
@@ -141,6 +149,30 @@ export const ADMIN_NAV_MENUS = [
},
];
+export const ADMIN_QA_NAV_MENUS = [
+ { name: 'questions', path: '/admin/qa/questions' },
+ { name: 'answers', path: '/admin/qa/answers' },
+ { name: 'settings', path: '/admin/qa/settings' },
+];
+
+export const ADMIN_TAGS_NAV_MENUS = [
+ // { name: 'tags', path: '/admin/tags' },
+ {
+ name: 'settings',
+ path: '/admin/tags/settings',
+ },
+];
+
+export const ADMIN_USERS_NAV_MENUS = [
+ { name: 'users', path: '/admin/users' },
+ { name: 'settings', path: '/admin/users/settings' },
+];
+
+export const ADMIN_RULES_NAV_MENUS = [
+ { name: 'privileges', path: '/admin/rules/privileges' },
+ { name: 'policies', path: '/admin/rules/policies' },
+];
+
export const TIMEZONES = [
{
label: 'Africa',
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 3a77047e..aac89cce 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -364,7 +364,6 @@ export interface AdminSettingsGeneral {
description: string;
site_url: string;
contact_email: string;
- check_update: boolean;
permalink?: number;
}
@@ -382,8 +381,6 @@ export interface HelmetUpdate extends Omit<HelmetBase,
'pageTitle'> {
export interface AdminSettingsInterface {
language: string;
time_zone?: string;
- default_avatar: string;
- gravatar_base_url: string;
}
export interface AdminSettingsSmtp {
@@ -405,6 +402,14 @@ export interface AdminSettingsUsers {
allow_update_location: boolean;
allow_update_username: boolean;
allow_update_website: boolean;
+ default_avatar: string;
+ gravatar_base_url: string;
+}
+
+export interface AdminSettingsSecurity {
+ external_content_display: string;
+ check_update: boolean;
+ login_required: boolean;
}
export interface SiteSettings {
@@ -416,10 +421,12 @@ export interface SiteSettings {
theme: AdminSettingsTheme;
site_seo: AdminSettingsSeo;
site_users: AdminSettingsUsers;
- site_write: AdminSettingsWrite;
+ site_advanced: AdminSettingsWrite;
+ site_questions: AdminQuestionSetting;
+ site_tags: AdminTagsSetting;
version: string;
revision: string;
- site_legal: AdminSettingsLegal;
+ site_security: AdminSettingsSecurity;
}
export interface AdminSettingBranding {
@@ -430,7 +437,6 @@ export interface AdminSettingBranding {
}
export interface AdminSettingsLegal {
- external_content_display: string;
privacy_policy_original_text?: string;
privacy_policy_parsed_text?: string;
terms_of_service_original_text?: string;
@@ -438,12 +444,6 @@ export interface AdminSettingsLegal {
}
export interface AdminSettingsWrite {
- restrict_answer?: boolean;
- min_tags?: number;
- min_content?: number;
- recommend_tags?: Tag[];
- required_tag?: boolean;
- reserved_tags?: Tag[];
max_image_size?: number;
max_attachment_size?: number;
max_image_megapixel?: number;
@@ -484,7 +484,6 @@ export interface AdminSettingsCustom {
export interface AdminSettingsLogin {
allow_new_registrations: boolean;
- login_required: boolean;
allow_email_registrations: boolean;
allow_email_domains: string[];
allow_password_login: boolean;
@@ -809,3 +808,15 @@ export interface BadgeDetailListRes {
count: number;
list: BadgeDetailListItem[];
}
+
+export interface AdminQuestionSetting {
+ min_tags: number;
+ min_content: number;
+ restrict_answer: boolean;
+}
+
+export interface AdminTagsSetting {
+ recommend_tags: Tag[];
+ required_tag: boolean;
+ reserved_tags: Tag[];
+}
diff --git a/ui/src/components/AccordionNav/index.tsx
b/ui/src/components/AccordionNav/index.tsx
index ee081978..583c0198 100644
--- a/ui/src/components/AccordionNav/index.tsx
+++ b/ui/src/components/AccordionNav/index.tsx
@@ -28,16 +28,32 @@ import { floppyNavigation } from '@/utils';
import { Icon } from '@/components';
import './index.css';
+export interface MenuItem {
+ name: string;
+ path?: string;
+ pathPrefix?: string;
+ icon?: string;
+ displayName?: string;
+ badgeContent?: string | number;
+ children?: MenuItem[];
+}
+
function MenuNode({
menu,
callback,
activeKey,
expanding = false,
path = '/',
+}: {
+ menu: MenuItem;
+ callback: (evt: any, menu: MenuItem, href: string, isLeaf: boolean) => void;
+ activeKey: string;
+ expanding?: boolean;
+ path?: string;
}) {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
- const isLeaf = !menu.children.length;
- const href = isLeaf ? `${path}${menu.path}` : '#';
+ const isLeaf = !menu.children || menu.children.length === 0;
+ const href = isLeaf ? `${path}${menu.path || ''}` : '#';
return (
<Nav.Item key={menu.path} className="w-100">
@@ -51,7 +67,14 @@ function MenuNode({
}}
className={classNames(
'text-nowrap d-flex flex-nowrap align-items-center w-100',
- { expanding, active: activeKey === menu.path },
+ {
+ expanding,
+ active:
+ activeKey === menu.path ||
+ (menu.path && activeKey.startsWith(`${menu.path}/`)) ||
+ // if pathPrefix is set, activate when activeKey starts with
the pathPrefix
+ (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),
+ },
)}>
{menu?.icon && <Icon name={menu.icon} className="me-2" />}
@@ -75,7 +98,13 @@ function MenuNode({
}}
className={classNames(
'text-nowrap d-flex flex-nowrap align-items-center w-100',
- { expanding, active: activeKey === menu.path },
+ {
+ expanding,
+ active:
+ activeKey === menu.path ||
+ (menu.path && activeKey.startsWith(`${menu.path}/`)) ||
+ (menu.pathPrefix && activeKey.startsWith(menu.pathPrefix)),
+ },
)}>
{menu?.icon && <Icon name={menu.icon} className="me-2" />}
<span className="me-auto text-truncate">
@@ -90,8 +119,8 @@ function MenuNode({
</Nav.Link>
)}
- {menu.children.length ? (
- <Accordion.Collapse eventKey={menu.path} className="ms-4">
+ {menu.children && menu.children.length > 0 ? (
+ <Accordion.Collapse eventKey={menu.path || menu.name} className="ms-4">
<>
{menu.children.map((leaf) => {
return (
@@ -100,7 +129,7 @@ function MenuNode({
callback={callback}
activeKey={activeKey}
path={path}
- key={leaf.path}
+ key={leaf.path || leaf.name}
/>
);
})}
@@ -112,7 +141,7 @@ function MenuNode({
}
interface AccordionProps {
- menus: any[];
+ menus: MenuItem[];
path?: string;
}
const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
@@ -137,19 +166,27 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [],
path = '/' }) => {
});
const splat = pathMatch && pathMatch.params['*'];
- let activeKey = menus[0].path;
+ let activeKey: string = menus[0]?.path || menus[0]?.name || '';
+
if (splat) {
activeKey = splat;
}
+
const getOpenKey = () => {
let openKey = '';
menus.forEach((li) => {
- if (li.children.length) {
+ if (li.children && li.children.length > 0) {
const matchedChild = li.children.find((el) => {
- return el.path === activeKey;
+ // exact match or path prefix match
+ return (
+ el.path === activeKey ||
+ (el.path && activeKey.startsWith(`${el.path}/`)) ||
+ // if pathPrefix is set, activate when activeKey starts with the
pathPrefix
+ (el.pathPrefix && activeKey.startsWith(el.pathPrefix))
+ );
});
if (matchedChild) {
- openKey = li.path;
+ openKey = li.path || li.name || '';
}
}
});
@@ -181,8 +218,8 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [],
path = '/' }) => {
path={path}
callback={menuClick}
activeKey={activeKey}
- expanding={openKey === li.path}
- key={li.path}
+ expanding={openKey === (li.path || li.name)}
+ key={li.path || li.name}
/>
);
})}
diff --git a/ui/src/components/AdminSideNav/index.tsx
b/ui/src/components/AdminSideNav/index.tsx
index a2d36fbd..b6c4d4bc 100644
--- a/ui/src/components/AdminSideNav/index.tsx
+++ b/ui/src/components/AdminSideNav/index.tsx
@@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next';
import cloneDeep from 'lodash/cloneDeep';
import { AccordionNav, Icon } from '@/components';
+import type { MenuItem } from '@/components/AccordionNav';
import { ADMIN_NAV_MENUS } from '@/common/constants';
import { useQueryPlugins } from '@/services';
import { interfaceStore } from '@/stores';
@@ -37,16 +38,18 @@ const AdminSideNav = () => {
have_config: true,
});
- const menus = cloneDeep(ADMIN_NAV_MENUS);
+ const menus = cloneDeep(ADMIN_NAV_MENUS) as MenuItem[];
if (configurablePlugins && configurablePlugins.length > 0) {
menus.forEach((item) => {
if (item.name === 'plugins' && item.children) {
item.children = [
...item.children,
- ...configurablePlugins.map((plugin) => ({
- name: plugin.slug_name,
- displayName: plugin.name,
- })),
+ ...configurablePlugins.map(
+ (plugin): MenuItem => ({
+ name: plugin.slug_name,
+ displayName: plugin.name,
+ }),
+ ),
];
}
});
diff --git a/ui/src/components/SchemaForm/components/Switch.tsx
b/ui/src/components/SchemaForm/components/Switch.tsx
index 336142ae..81f928d3 100644
--- a/ui/src/components/SchemaForm/components/Switch.tsx
+++ b/ui/src/components/SchemaForm/components/Switch.tsx
@@ -51,6 +51,7 @@ const Index: FC<Props> = ({
onChange(state);
}
};
+
return (
<Form.Check
name={fieldName}
@@ -59,7 +60,7 @@ const Index: FC<Props> = ({
checked={fieldObject?.value || ''}
feedback={fieldObject?.errorMsg}
feedbackType="invalid"
- isInvalid={fieldObject.isInvalid}
+ isInvalid={fieldObject?.isInvalid}
disabled={readOnly}
onChange={handleChange}
/>
diff --git a/ui/src/components/SchemaForm/index.tsx
b/ui/src/components/SchemaForm/index.tsx
index 171b5f67..01f50681 100644
--- a/ui/src/components/SchemaForm/index.tsx
+++ b/ui/src/components/SchemaForm/index.tsx
@@ -290,7 +290,7 @@ const SchemaForm: ForwardRefRenderFunction<FormRef,
FormProps> = (
controlId={key}
className={classnames(
groupClassName,
- formData[key].hidden ? 'd-none' : null,
+ formData[key]?.hidden ? 'd-none' : null,
)}>
{/* Uniform processing `label` */}
{title && !uiSimplify ? <Form.Label>{title}</Form.Label> : null}
@@ -437,12 +437,12 @@ const SchemaForm: ForwardRefRenderFunction<FormRef,
FormProps> = (
/>
) : null}
{/* Unified handling of `Feedback` and `Text` */}
- <Form.Control.Feedback type="invalid">
- {fieldState?.errorMsg}
- </Form.Control.Feedback>
{description && widget !== 'tag_selector' ? (
<Form.Text dangerouslySetInnerHTML={{ __html: description }} />
) : null}
+ <Form.Control.Feedback type="invalid">
+ {fieldState?.errorMsg}
+ </Form.Control.Feedback>
</Form.Group>
);
})}
diff --git a/ui/src/components/SchemaForm/types.ts
b/ui/src/components/SchemaForm/types.ts
index 55cf8e99..25e5c56c 100644
--- a/ui/src/components/SchemaForm/types.ts
+++ b/ui/src/components/SchemaForm/types.ts
@@ -44,7 +44,7 @@ export interface JSONSchema {
required?: string[];
properties: {
[key: string]: {
- type?: 'string' | 'boolean' | 'number';
+ type?: 'string' | 'boolean' | 'number' | Type.Tag[];
title: string;
description?: string;
enum?: Array<string | boolean | number>;
diff --git a/ui/src/components/TabNav/index.tsx
b/ui/src/components/TabNav/index.tsx
new file mode 100644
index 00000000..482cd0cf
--- /dev/null
+++ b/ui/src/components/TabNav/index.tsx
@@ -0,0 +1,26 @@
+import { FC } from 'react';
+import { Nav } from 'react-bootstrap';
+import { NavLink, useLocation } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+
+const TabNav: FC<{ menus: { name: string; path: string }[] }> = ({ menus }) =>
{
+ const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
+ const { pathname } = useLocation();
+ return (
+ <Nav variant="underline" className="mb-4 border-bottom">
+ {menus.map((menu) => (
+ <Nav.Item key={menu.path}>
+ <NavLink
+ to={menu.path}
+ className={() =>
+ pathname === menu.path ? 'nav-link active' : 'nav-link'
+ }>
+ {t(menu.name)}
+ </NavLink>
+ </Nav.Item>
+ ))}
+ </Nav>
+ );
+};
+
+export default TabNav;
diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts
index 68e863d2..416db241 100644
--- a/ui/src/components/index.ts
+++ b/ui/src/components/index.ts
@@ -64,6 +64,7 @@ import CardBadge from './CardBadge';
import PinList from './PinList';
import MobileSideNav from './MobileSideNav';
import AdminSideNav from './AdminSideNav';
+import TabNav from './TabNav';
export {
Avatar,
@@ -115,5 +116,6 @@ export {
PinList,
MobileSideNav,
AdminSideNav,
+ TabNav,
};
export type { EditorRef, JSONSchema, UISchema };
diff --git a/ui/src/pages/Admin/Answers/index.tsx
b/ui/src/pages/Admin/Answers/index.tsx
index 07190024..9909739f 100644
--- a/ui/src/pages/Admin/Answers/index.tsx
+++ b/ui/src/pages/Admin/Answers/index.tsx
@@ -32,8 +32,9 @@ import {
Empty,
QueryGroup,
Modal,
+ TabNav,
} from '@/components';
-import { ADMIN_LIST_STATUS } from '@/common/constants';
+import { ADMIN_LIST_STATUS, ADMIN_QA_NAV_MENUS } from '@/common/constants';
import * as Type from '@/common/interface';
import { deletePermanently, useAnswerSearch } from '@/services';
import { escapeRemove } from '@/utils';
@@ -96,7 +97,10 @@ const Answers: FC = () => {
};
return (
<>
- <h3 className="mb-4">{t('page_title')}</h3>
+ <h3 className="mb-4">
+ {t('page_title', { keyPrefix: 'admin.questions' })}
+ </h3>
+ <TabNav menus={ADMIN_QA_NAV_MENUS} />
<div className="d-flex flex-wrap justify-content-between
align-items-center">
<Stack direction="horizontal" gap={3} className="mb-3">
<QueryGroup
diff --git a/ui/src/pages/Admin/Branding/index.tsx
b/ui/src/pages/Admin/Branding/index.tsx
index bec7fc0f..ce2417cb 100644
--- a/ui/src/pages/Admin/Branding/index.tsx
+++ b/ui/src/pages/Admin/Branding/index.tsx
@@ -169,13 +169,15 @@ const Index: FC = () => {
return (
<ImgViewer>
<h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- uiSchema={uiSchema}
- formData={formData}
- onSubmit={onSubmit}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onSubmit={onSubmit}
+ onChange={handleOnChange}
+ />
+ </div>
</ImgViewer>
);
};
diff --git a/ui/src/pages/Admin/CssAndHtml/index.tsx
b/ui/src/pages/Admin/CssAndHtml/index.tsx
index 422659af..557ffa3c 100644
--- a/ui/src/pages/Admin/CssAndHtml/index.tsx
+++ b/ui/src/pages/Admin/CssAndHtml/index.tsx
@@ -151,13 +151,15 @@ const Index: FC = () => {
return (
<>
<h3 className="mb-4">{t('customize', { keyPrefix: 'nav_menus' })}</h3>
- <SchemaForm
- schema={schema}
- formData={formData}
- onSubmit={onSubmit}
- uiSchema={uiSchema}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ formData={formData}
+ onSubmit={onSubmit}
+ uiSchema={uiSchema}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
index 137fcd2e..9a79edd9 100644
--- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
+++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import type * as Type from '@/common/interface';
-import { siteInfoStore } from '@/stores';
+import { siteSecurityStore } from '@/stores';
const { gt, gte } = require('semver');
@@ -34,7 +34,7 @@ interface IProps {
const HealthStatus: FC<IProps> = ({ data }) => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard'
});
const { version, remote_version } = data.version_info || {};
- const { siteInfo } = siteInfoStore();
+ const { check_update } = siteSecurityStore.getState();
let isLatest = false;
let hasNewerVersion = false;
const downloadUrl =
`https://answer.apache.org/download?from_version=${version}`;
@@ -68,7 +68,7 @@ const HealthStatus: FC<IProps> = ({ data }) => {
{t('update_to')} {remote_version}
</a>
)}
- {!isLatest && !remote_version && siteInfo.check_update && (
+ {!isLatest && !remote_version && check_update && (
<a
className="ms-1 badge rounded-pill text-bg-danger"
target="_blank"
diff --git a/ui/src/pages/Admin/Files/index.tsx
b/ui/src/pages/Admin/Files/index.tsx
new file mode 100644
index 00000000..8c8650e8
--- /dev/null
+++ b/ui/src/pages/Admin/Files/index.tsx
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FC, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Form, Button } from 'react-bootstrap';
+
+import type * as Type from '@/common/interface';
+import { useToast } from '@/hooks';
+import { getAdminFilesSetting, updateAdminFilesSetting } from '@/services';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { writeSettingStore } from '@/stores';
+
+const initFormData = {
+ max_image_size: {
+ value: 0,
+ errorMsg: '',
+ isInvalid: false,
+ },
+ max_attachment_size: {
+ value: 0,
+ errorMsg: '',
+ isInvalid: false,
+ },
+ max_image_megapixel: {
+ value: 0,
+ errorMsg: '',
+ isInvalid: false,
+ },
+ authorized_image_extensions: {
+ value: '',
+ errorMsg: '',
+ isInvalid: false,
+ },
+ authorized_attachment_extensions: {
+ value: '',
+ errorMsg: '',
+ isInvalid: false,
+ },
+};
+
+const Index: FC = () => {
+ const { t } = useTranslation('translation', {
+ keyPrefix: 'admin.write',
+ });
+ const Toast = useToast();
+
+ const [formData, setFormData] = useState(initFormData);
+
+ const handleValueChange = (value) => {
+ setFormData({
+ ...formData,
+ ...value,
+ });
+ };
+
+ const onSubmit = (evt) => {
+ evt.preventDefault();
+ evt.stopPropagation();
+
+ const reqParams: Type.AdminSettingsWrite = {
+ max_image_size: Number(formData.max_image_size.value),
+ max_attachment_size: Number(formData.max_attachment_size.value),
+ max_image_megapixel: Number(formData.max_image_megapixel.value),
+ authorized_image_extensions:
+ formData.authorized_image_extensions.value?.length > 0
+ ? formData.authorized_image_extensions.value
+ .split(',')
+ ?.map((item) => item.trim().toLowerCase())
+ : [],
+ authorized_attachment_extensions:
+ formData.authorized_attachment_extensions.value?.length > 0
+ ? formData.authorized_attachment_extensions.value
+ .split(',')
+ ?.map((item) => item.trim().toLowerCase())
+ : [],
+ };
+ updateAdminFilesSetting(reqParams)
+ .then(() => {
+ Toast.onShow({
+ msg: t('update', { keyPrefix: 'toast' }),
+ variant: 'success',
+ });
+ writeSettingStore.getState().update({ ...reqParams });
+ })
+ .catch((err) => {
+ if (err.isError) {
+ const data = handleFormError(err, formData);
+ setFormData({ ...data });
+ const ele = document.getElementById(err.list[0].error_field);
+ scrollToElementTop(ele);
+ }
+ });
+ };
+
+ const initData = () => {
+ getAdminFilesSetting().then((res) => {
+ formData.max_image_size.value = res.max_image_size;
+ formData.max_attachment_size.value = res.max_attachment_size;
+ formData.max_image_megapixel.value = res.max_image_megapixel;
+ formData.authorized_image_extensions.value =
+ res.authorized_image_extensions?.join(', ').toLowerCase();
+ formData.authorized_attachment_extensions.value =
+ res.authorized_attachment_extensions?.join(', ').toLowerCase();
+ setFormData({ ...formData });
+ });
+ };
+
+ useEffect(() => {
+ initData();
+ }, []);
+
+ return (
+ <>
+ <h3 className="mb-4">{t('page_title')}</h3>
+ <div className="max-w-748">
+ <Form noValidate onSubmit={onSubmit}>
+ <Form.Group className="mb-3" controlId="max_image_size">
+ <Form.Label>{t('image_size.label')}</Form.Label>
+ <Form.Control
+ type="number"
+ inputMode="numeric"
+ min={0}
+ value={formData.max_image_size.value}
+ isInvalid={formData.max_image_size.isInvalid}
+ onChange={(evt) => {
+ handleValueChange({
+ max_image_size: {
+ value: evt.target.value,
+ errorMsg: '',
+ isInvalid: false,
+ },
+ });
+ }}
+ />
+ <Form.Text>{t('image_size.text')}</Form.Text>
+ <Form.Control.Feedback type="invalid">
+ {formData.max_image_size.errorMsg}
+ </Form.Control.Feedback>
+ </Form.Group>
+
+ <Form.Group className="mb-3" controlId="max_attachment_size">
+ <Form.Label>{t('attachment_size.label')}</Form.Label>
+ <Form.Control
+ type="number"
+ inputMode="numeric"
+ min={0}
+ value={formData.max_attachment_size.value}
+ isInvalid={formData.max_attachment_size.isInvalid}
+ onChange={(evt) => {
+ handleValueChange({
+ max_attachment_size: {
+ value: evt.target.value,
+ errorMsg: '',
+ isInvalid: false,
+ },
+ });
+ }}
+ />
+ <Form.Text>{t('attachment_size.text')}</Form.Text>
+ <Form.Control.Feedback type="invalid">
+ {formData.max_attachment_size.errorMsg}
+ </Form.Control.Feedback>
+ </Form.Group>
+
+ <Form.Group className="mb-3" controlId="max_image_megapixel">
+ <Form.Label>{t('image_megapixels.label')}</Form.Label>
+ <Form.Control
+ type="number"
+ inputMode="numeric"
+ min={0}
+ isInvalid={formData.max_image_megapixel.isInvalid}
+ value={formData.max_image_megapixel.value}
+ onChange={(evt) => {
+ handleValueChange({
+ max_image_megapixel: {
+ value: evt.target.value,
+ errorMsg: '',
+ isInvalid: false,
+ },
+ });
+ }}
+ />
+ <Form.Text>{t('image_megapixels.text')}</Form.Text>
+ <Form.Control.Feedback type="invalid">
+ {formData.max_image_megapixel.errorMsg}
+ </Form.Control.Feedback>
+ </Form.Group>
+
+ <Form.Group className="mb-3" controlId="authorized_image_extensions">
+ <Form.Label>{t('image_extensions.label')}</Form.Label>
+ <Form.Control
+ type="text"
+ value={formData.authorized_image_extensions.value}
+ isInvalid={formData.authorized_image_extensions.isInvalid}
+ onChange={(evt) => {
+ handleValueChange({
+ authorized_image_extensions: {
+ value: evt.target.value.toLowerCase(),
+ errorMsg: '',
+ isInvalid: false,
+ },
+ });
+ }}
+ />
+ <Form.Text>{t('image_extensions.text')}</Form.Text>
+ <Form.Control.Feedback type="invalid">
+ {formData.authorized_image_extensions.errorMsg}
+ </Form.Control.Feedback>
+ </Form.Group>
+
+ <Form.Group
+ className="mb-3"
+ controlId="authorized_attachment_extensions">
+ <Form.Label>{t('attachment_extensions.label')}</Form.Label>
+ <Form.Control
+ type="text"
+ value={formData.authorized_attachment_extensions.value}
+ isInvalid={formData.authorized_attachment_extensions.isInvalid}
+ onChange={(evt) => {
+ handleValueChange({
+ authorized_attachment_extensions: {
+ value: evt.target.value.toLowerCase(),
+ errorMsg: '',
+ isInvalid: false,
+ },
+ });
+ }}
+ />
+ <Form.Text>{t('attachment_extensions.text')}</Form.Text>
+ <Form.Control.Feedback type="invalid">
+ {formData.authorized_attachment_extensions.errorMsg}
+ </Form.Control.Feedback>
+ </Form.Group>
+
+ <Form.Group className="mb-3">
+ <Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
+ </Form.Group>
+ </Form>
+ </div>
+ </>
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Admin/General/index.tsx
b/ui/src/pages/Admin/General/index.tsx
index ce2cdeb5..57602f45 100644
--- a/ui/src/pages/Admin/General/index.tsx
+++ b/ui/src/pages/Admin/General/index.tsx
@@ -70,11 +70,6 @@ const General: FC = () => {
title: t('contact_email.label'),
description: t('contact_email.text'),
},
- check_update: {
- type: 'boolean',
- title: t('check_update.label'),
- default: true,
- },
},
};
const uiSchema: UISchema = {
@@ -114,12 +109,6 @@ const General: FC = () => {
},
},
},
- check_update: {
- 'ui:widget': 'switch',
- 'ui:options': {
- label: t('check_update.text'),
- },
- },
};
const [formData, setFormData] = useState<Type.FormDataType>(
initFormData(schema),
@@ -134,7 +123,6 @@ const General: FC = () => {
short_description: formData.short_description.value,
site_url: formData.site_url.value,
contact_email: formData.contact_email.value,
- check_update: formData.check_update.value,
};
updateGeneralSetting(reqParams)
@@ -149,7 +137,6 @@ const General: FC = () => {
formData.short_description.value = res.short_description;
formData.site_url.value = res.site_url;
formData.contact_email.value = res.contact_email;
- formData.check_update.value = res.check_update;
}
setFormData({ ...formData });
@@ -183,13 +170,15 @@ const General: FC = () => {
return (
<>
<h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- formData={formData}
- onSubmit={onSubmit}
- uiSchema={uiSchema}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ formData={formData}
+ onSubmit={onSubmit}
+ uiSchema={uiSchema}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Interface/index.tsx
b/ui/src/pages/Admin/Interface/index.tsx
index 865029e9..49e5175d 100644
--- a/ui/src/pages/Admin/Interface/index.tsx
+++ b/ui/src/pages/Admin/Interface/index.tsx
@@ -28,7 +28,7 @@ import {
} from '@/common/interface';
import { interfaceStore, loggedUserInfoStore } from '@/stores';
import { JSONSchema, SchemaForm, UISchema } from '@/components';
-import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
+import { DEFAULT_TIMEZONE } from '@/common/constants';
import {
updateInterfaceSetting,
useInterfaceSetting,
@@ -68,20 +68,6 @@ const Interface: FC = () => {
description: t('time_zone.text'),
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),
- default: setting?.default_avatar || 'system',
- },
- gravatar_base_url: {
- type: 'string',
- title: t('gravatar_base_url.label'),
- description: t('gravatar_base_url.text'),
- default: setting?.gravatar_base_url || '',
- },
},
};
@@ -96,16 +82,6 @@ const Interface: FC = () => {
isInvalid: false,
errorMsg: '',
},
- default_avatar: {
- value: setting?.default_avatar || 'system',
- isInvalid: false,
- errorMsg: '',
- },
- gravatar_base_url: {
- value: setting?.gravatar_base_url || '',
- isInvalid: false,
- errorMsg: '',
- },
});
const uiSchema: UISchema = {
@@ -115,15 +91,6 @@ const Interface: FC = () => {
time_zone: {
'ui:widget': 'timezone',
},
- default_avatar: {
- 'ui:widget': 'select',
- },
- gravatar_base_url: {
- 'ui:widget': 'input',
- 'ui:options': {
- placeholder: 'https://www.gravatar.com/avatar/',
- },
- },
};
const getLangs = async () => {
const res: LangsType[] = await loadLanguageOptions(true);
@@ -156,8 +123,6 @@ const Interface: FC = () => {
const reqParams: AdminSettingsInterface = {
language: formData.language.value,
time_zone: formData.time_zone.value,
- default_avatar: formData.default_avatar.value,
- gravatar_base_url: formData.gravatar_base_url.value,
};
updateInterfaceSetting(reqParams)
@@ -185,20 +150,18 @@ const Interface: FC = () => {
useEffect(() => {
if (setting) {
- const formMeta = {};
- Object.keys(setting).forEach((k) => {
- let v = setting[k];
- if (k === 'default_avatar' && !v) {
- v = 'system';
- }
- if (k === 'gravatar_base_url' && !v) {
- v = '';
- }
- formMeta[k] = { ...formData[k], value: v };
- });
+ const formMeta = { ...formData };
+ if (setting.language) {
+ formMeta.language.value = setting.language;
+ } else {
+ formMeta.language.value = storeInterface.language || langs?.[0]?.value;
+ }
+ if (setting.time_zone) {
+ formMeta.time_zone.value = setting.time_zone;
+ }
setFormData({ ...formData, ...formMeta });
}
- }, [setting]);
+ }, [setting, langs]);
useEffect(() => {
getLangs();
}, []);
@@ -209,13 +172,15 @@ const Interface: FC = () => {
return (
<>
<h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- uiSchema={uiSchema}
- formData={formData}
- onSubmit={onSubmit}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onSubmit={onSubmit}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Login/index.tsx
b/ui/src/pages/Admin/Login/index.tsx
index c43cfcff..a4a61152 100644
--- a/ui/src/pages/Admin/Login/index.tsx
+++ b/ui/src/pages/Admin/Login/index.tsx
@@ -58,12 +58,6 @@ const Index: FC = () => {
title: t('allowed_email_domains.title'),
description: t('allowed_email_domains.text'),
},
- login_required: {
- type: 'boolean',
- title: t('private.title'),
- description: t('private.text'),
- default: false,
- },
},
};
const uiSchema: UISchema = {
@@ -88,12 +82,6 @@ const Index: FC = () => {
allow_email_domains: {
'ui:widget': 'textarea',
},
- login_required: {
- 'ui:widget': 'switch',
- 'ui:options': {
- label: t('private.label'),
- },
- },
};
const [formData, setFormData] = useState(initFormData(schema));
const { update: updateLoginSetting } = loginSettingStore((_) => _);
@@ -116,7 +104,6 @@ const Index: FC = () => {
allow_new_registrations: formData.allow_new_registrations.value,
allow_email_registrations: formData.allow_email_registrations.value,
allow_email_domains: allowedEmailDomains,
- login_required: formData.login_required.value,
allow_password_login: formData.allow_password_login.value,
};
@@ -151,7 +138,6 @@ const Index: FC = () => {
formMeta.allow_email_domains.value =
setting.allow_email_domains.join('\n');
}
- formMeta.login_required.value = setting.login_required;
formMeta.allow_password_login.value = setting.allow_password_login;
setFormData({ ...formMeta });
}
@@ -165,13 +151,15 @@ const Index: FC = () => {
return (
<>
<h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- formData={formData}
- onSubmit={onSubmit}
- uiSchema={uiSchema}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ formData={formData}
+ onSubmit={onSubmit}
+ uiSchema={uiSchema}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Plugins/Config/index.tsx
b/ui/src/pages/Admin/Plugins/Config/index.tsx
index 7c47d016..44d83ad6 100644
--- a/ui/src/pages/Admin/Plugins/Config/index.tsx
+++ b/ui/src/pages/Admin/Plugins/Config/index.tsx
@@ -114,14 +114,16 @@ const Config = () => {
return (
<>
<h3 className="mb-4">{data?.name}</h3>
- <SchemaForm
- schema={schema}
- uiSchema={uiSchema}
- refreshConfig={refreshConfig}
- formData={formData}
- onSubmit={onSubmit}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ refreshConfig={refreshConfig}
+ formData={formData}
+ onSubmit={onSubmit}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Legal/index.tsx
b/ui/src/pages/Admin/Policies/index.tsx
similarity index 70%
rename from ui/src/pages/Admin/Legal/index.tsx
rename to ui/src/pages/Admin/Policies/index.tsx
index 4a4e4ea1..7170c39f 100644
--- a/ui/src/pages/Admin/Legal/index.tsx
+++ b/ui/src/pages/Admin/Policies/index.tsx
@@ -23,11 +23,17 @@ import { useTranslation } from 'react-i18next';
import { marked } from 'marked';
import type * as Type from '@/common/interface';
-import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
+import {
+ SchemaForm,
+ JSONSchema,
+ initFormData,
+ UISchema,
+ TabNav,
+} from '@/components';
import { useToast } from '@/hooks';
-import { getLegalSetting, putLegalSetting } from '@/services';
+import { getPoliciesSetting, putPoliciesSetting } from '@/services';
import { handleFormError, scrollToElementTop } from '@/utils';
-import { siteLealStore } from '@/stores';
+import { ADMIN_RULES_NAV_MENUS } from '@/common/constants';
const Legal: FC = () => {
const { t } = useTranslation('translation', {
@@ -35,29 +41,10 @@ const Legal: FC = () => {
});
const Toast = useToast();
- const externalContent = [
- {
- value: 'always_display',
- label: t('external_content_display.always_display'),
- },
- {
- value: 'ask_before_display',
- label: t('external_content_display.ask_before_display'),
- },
- ];
-
const schema: JSONSchema = {
title: t('page_title'),
required: ['terms_of_service', 'privacy_policy'],
properties: {
- external_content_display: {
- type: 'string',
- title: t('external_content_display.label'),
- description: t('external_content_display.text'),
- enum: externalContent?.map((lang) => lang.value),
- enumNames: externalContent?.map((lang) => lang.label),
- default: 0,
- },
terms_of_service: {
type: 'string',
title: t('terms_of_service.label'),
@@ -71,9 +58,6 @@ const Legal: FC = () => {
},
};
const uiSchema: UISchema = {
- external_content_display: {
- 'ui:widget': 'select',
- },
terms_of_service: {
'ui:widget': 'textarea',
'ui:options': {
@@ -94,7 +78,6 @@ const Legal: FC = () => {
evt.stopPropagation();
const reqParams: Type.AdminSettingsLegal = {
- external_content_display: formData.external_content_display.value,
terms_of_service_original_text: formData.terms_of_service.value,
terms_of_service_parsed_text: marked.parse(
formData.terms_of_service.value,
@@ -103,15 +86,12 @@ const Legal: FC = () => {
privacy_policy_parsed_text: marked.parse(formData.privacy_policy.value),
};
- putLegalSetting(reqParams)
+ putPoliciesSetting(reqParams)
.then(() => {
Toast.onShow({
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
});
- siteLealStore.getState().update({
- external_content_display: reqParams.external_content_display,
- });
})
.catch((err) => {
if (err.isError) {
@@ -124,11 +104,9 @@ const Legal: FC = () => {
};
useEffect(() => {
- getLegalSetting().then((setting) => {
+ getPoliciesSetting().then((setting) => {
if (setting) {
const formMeta = { ...formData };
- formMeta.external_content_display.value =
- setting.external_content_display;
formMeta.terms_of_service.value =
setting.terms_of_service_original_text;
formMeta.privacy_policy.value = setting.privacy_policy_original_text;
@@ -143,14 +121,17 @@ const Legal: FC = () => {
return (
<>
- <h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- formData={formData}
- onSubmit={onSubmit}
- uiSchema={uiSchema}
- onChange={handleOnChange}
- />
+ <h3 className="mb-4">{t('rules', { keyPrefix: 'nav_menus' })}</h3>
+ <TabNav menus={ADMIN_RULES_NAV_MENUS} />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ formData={formData}
+ onSubmit={onSubmit}
+ uiSchema={uiSchema}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Privileges/index.tsx
b/ui/src/pages/Admin/Privileges/index.tsx
index f0930c7a..9ab775dc 100644
--- a/ui/src/pages/Admin/Privileges/index.tsx
+++ b/ui/src/pages/Admin/Privileges/index.tsx
@@ -22,7 +22,13 @@ import { useTranslation } from 'react-i18next';
import { useToast } from '@/hooks';
import { FormDataType } from '@/common/interface';
-import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
+import {
+ JSONSchema,
+ SchemaForm,
+ UISchema,
+ initFormData,
+ TabNav,
+} from '@/components';
import {
getPrivilegeSetting,
putPrivilegeSetting,
@@ -30,7 +36,10 @@ import {
AdminSettingsPrivilegeReq,
} from '@/services';
import { handleFormError, scrollToElementTop } from '@/utils';
-import { ADMIN_PRIVILEGE_CUSTOM_LEVEL } from '@/common/constants';
+import {
+ ADMIN_PRIVILEGE_CUSTOM_LEVEL,
+ ADMIN_RULES_NAV_MENUS,
+} from '@/common/constants';
const Index: FC = () => {
const { t } = useTranslation('translation', {
@@ -187,14 +196,17 @@ const Index: FC = () => {
return (
<>
- <h3 className="mb-4">{t('title')}</h3>
- <SchemaForm
- schema={schema}
- uiSchema={uiSchema}
- formData={formData}
- onSubmit={onSubmit}
- onChange={handleOnChange}
- />
+ <h3 className="mb-4">{t('rules', { keyPrefix: 'nav_menus' })}</h3>
+ <TabNav menus={ADMIN_RULES_NAV_MENUS} />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onSubmit={onSubmit}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/QaSettings/index.tsx
b/ui/src/pages/Admin/QaSettings/index.tsx
new file mode 100644
index 00000000..0c2636ea
--- /dev/null
+++ b/ui/src/pages/Admin/QaSettings/index.tsx
@@ -0,0 +1,132 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import {
+ SchemaForm,
+ JSONSchema,
+ UISchema,
+ initFormData,
+ TabNav,
+} from '@/components';
+import { ADMIN_QA_NAV_MENUS } from '@/common/constants';
+import * as Type from '@/common/interface';
+import { writeSettingStore } from '@/stores';
+import {
+ getQuestionSetting,
+ updateQuestionSetting,
+} from '@/services/admin/question';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { useToast } from '@/hooks';
+
+const QaSettings = () => {
+ const { t } = useTranslation('translation', {
+ keyPrefix: 'admin.write',
+ });
+ const Toast = useToast();
+ const schema: JSONSchema = {
+ title: t('page_title'),
+ properties: {
+ min_tags: {
+ type: 'number',
+ title: t('min_tags.label'),
+ description: t('min_tags.text'),
+ },
+ min_content: {
+ type: 'number',
+ title: t('min_content.label'),
+ description: t('min_content.text'),
+ },
+ restrict_answer: {
+ type: 'boolean',
+ title: t('restrict_answer.label'),
+ description: t('restrict_answer.text'),
+ },
+ },
+ };
+ const uiSchema: UISchema = {
+ min_tags: {
+ 'ui:widget': 'input',
+ 'ui:options': {
+ inputType: 'number',
+ },
+ },
+ min_content: {
+ 'ui:widget': 'input',
+ 'ui:options': {
+ inputType: 'number',
+ },
+ },
+ restrict_answer: {
+ 'ui:widget': 'switch',
+ 'ui:options': {
+ label: t('restrict_answer.label'),
+ },
+ },
+ };
+ const [formData, setFormData] = useState<Type.FormDataType>(
+ initFormData(schema),
+ );
+
+ const handleValueChange = (data: Type.FormDataType) => {
+ setFormData(data);
+ };
+
+ const onSubmit = (evt) => {
+ evt.preventDefault();
+ evt.stopPropagation();
+ // TODO: submit data
+ const reqParams: Type.AdminQuestionSetting = {
+ min_tags: formData.min_tags.value,
+ min_content: formData.min_content.value,
+ restrict_answer: formData.restrict_answer.value,
+ };
+ updateQuestionSetting(reqParams)
+ .then(() => {
+ Toast.onShow({
+ msg: t('update', { keyPrefix: 'toast' }),
+ variant: 'success',
+ });
+ writeSettingStore.getState().update({ ...reqParams });
+ })
+ .catch((err) => {
+ if (err.isError) {
+ const data = handleFormError(err, formData);
+ setFormData({ ...data });
+ const ele = document.getElementById(err.list[0].error_field);
+ scrollToElementTop(ele);
+ }
+ });
+ };
+
+ useEffect(() => {
+ getQuestionSetting().then((res) => {
+ if (res) {
+ const formMeta = { ...formData };
+ formMeta.min_tags.value = res.min_tags;
+ formMeta.min_content.value = res.min_content;
+ formMeta.restrict_answer.value = res.restrict_answer;
+ setFormData(formMeta);
+ }
+ });
+ }, []);
+
+ return (
+ <>
+ <h3 className="mb-4">
+ {t('page_title', { keyPrefix: 'admin.questions' })}
+ </h3>
+ <TabNav menus={ADMIN_QA_NAV_MENUS} />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onChange={handleValueChange}
+ onSubmit={onSubmit}
+ />
+ </div>
+ </>
+ );
+};
+
+export default QaSettings;
diff --git a/ui/src/pages/Admin/Questions/index.tsx
b/ui/src/pages/Admin/Questions/index.tsx
index 494effe6..ffd9f609 100644
--- a/ui/src/pages/Admin/Questions/index.tsx
+++ b/ui/src/pages/Admin/Questions/index.tsx
@@ -32,8 +32,9 @@ import {
Empty,
QueryGroup,
Modal,
+ TabNav,
} from '@/components';
-import { ADMIN_LIST_STATUS } from '@/common/constants';
+import { ADMIN_LIST_STATUS, ADMIN_QA_NAV_MENUS } from '@/common/constants';
import * as Type from '@/common/interface';
import { deletePermanently, useQuestionSearch } from '@/services';
import { pathFactory } from '@/router/pathFactory';
@@ -95,6 +96,7 @@ const Questions: FC = () => {
return (
<>
<h3 className="mb-4">{t('page_title')}</h3>
+ <TabNav menus={ADMIN_QA_NAV_MENUS} />
<div className="d-flex flex-wrap justify-content-between
align-items-center">
<Stack direction="horizontal" gap={3} className="mb-3">
<QueryGroup
diff --git a/ui/src/pages/Admin/Security/index.tsx
b/ui/src/pages/Admin/Security/index.tsx
new file mode 100644
index 00000000..07eaeb5b
--- /dev/null
+++ b/ui/src/pages/Admin/Security/index.tsx
@@ -0,0 +1,145 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import type * as Type from '@/common/interface';
+import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
+import { siteSecurityStore } from '@/stores';
+import {
+ getSecuritySetting,
+ putSecuritySetting,
+} from '@/services/admin/settings';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { useToast } from '@/hooks';
+
+const Security = () => {
+ const { t } = useTranslation('translation', {
+ keyPrefix: 'admin.security',
+ });
+ const Toast = useToast();
+ const externalContent = [
+ {
+ value: 'always_display',
+ label: t('external_content_display.always_display', {
+ keyPrefix: 'admin.legal',
+ }),
+ },
+ {
+ value: 'ask_before_display',
+ label: t('external_content_display.ask_before_display', {
+ keyPrefix: 'admin.legal',
+ }),
+ },
+ ];
+
+ const schema: JSONSchema = {
+ title: t('page_title'),
+ properties: {
+ login_required: {
+ type: 'boolean',
+ title: t('private.title', { keyPrefix: 'admin.login' }),
+ description: t('private.text', { keyPrefix: 'admin.login' }),
+ default: false,
+ },
+ external_content_display: {
+ type: 'string',
+ title: t('external_content_display.label', {
+ keyPrefix: 'admin.legal',
+ }),
+ description: t('external_content_display.text', {
+ keyPrefix: 'admin.legal',
+ }),
+ enum: externalContent?.map((lang) => lang.value),
+ enumNames: externalContent?.map((lang) => lang.label),
+ default: 0,
+ },
+ check_update: {
+ type: 'boolean',
+ title: t('check_update.label', { keyPrefix: 'admin.general' }),
+ default: true,
+ },
+ },
+ };
+ const uiSchema: UISchema = {
+ login_required: {
+ 'ui:widget': 'switch',
+ 'ui:options': {
+ label: t('private.label', { keyPrefix: 'admin.login' }),
+ },
+ },
+ external_content_display: {
+ 'ui:widget': 'select',
+ 'ui:options': {
+ label: t('external_content_display.label', {
+ keyPrefix: 'admin.legal',
+ }),
+ },
+ },
+ check_update: {
+ 'ui:widget': 'switch',
+ 'ui:options': {
+ label: t('check_update.label', { keyPrefix: 'admin.general' }),
+ },
+ },
+ };
+ const [formData, setFormData] = useState(initFormData(schema));
+
+ const handleValueChange = (data: Type.FormDataType) => {
+ setFormData(data);
+ };
+
+ const onSubmit = (evt) => {
+ evt.preventDefault();
+ evt.stopPropagation();
+ const reqParams = {
+ login_required: formData.login_required.value,
+ external_content_display: formData.external_content_display.value,
+ check_update: formData.check_update.value,
+ };
+ putSecuritySetting(reqParams)
+ .then(() => {
+ Toast.onShow({
+ msg: t('update', { keyPrefix: 'toast' }),
+ variant: 'success',
+ });
+ siteSecurityStore.getState().update(reqParams);
+ })
+ .catch((err) => {
+ if (err.isError) {
+ const data = handleFormError(err, formData);
+ setFormData({ ...data });
+ const ele = document.getElementById(err.list[0].error_field);
+ scrollToElementTop(ele);
+ }
+ });
+ };
+
+ useEffect(() => {
+ getSecuritySetting().then((setting) => {
+ if (setting) {
+ const formMeta = { ...formData };
+ formMeta.login_required.value = setting.login_required;
+ formMeta.external_content_display.value =
+ setting.external_content_display;
+ formMeta.check_update.value = setting.check_update;
+ setFormData(formMeta);
+ }
+ });
+ }, []);
+
+ return (
+ <>
+ <h3 className="mb-4">{t('security', { keyPrefix: 'nav_menus' })}</h3>
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onChange={handleValueChange}
+ onSubmit={onSubmit}
+ />
+ </div>
+ </>
+ );
+};
+
+export default Security;
diff --git a/ui/src/pages/Admin/Seo/index.tsx b/ui/src/pages/Admin/Seo/index.tsx
index e539595b..0675479d 100644
--- a/ui/src/pages/Admin/Seo/index.tsx
+++ b/ui/src/pages/Admin/Seo/index.tsx
@@ -117,13 +117,15 @@ const Index: FC = () => {
return (
<>
<h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- formData={formData}
- onSubmit={onSubmit}
- uiSchema={uiSchema}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ formData={formData}
+ onSubmit={onSubmit}
+ uiSchema={uiSchema}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Smtp/index.tsx
b/ui/src/pages/Admin/Smtp/index.tsx
index 85a1b976..59538751 100644
--- a/ui/src/pages/Admin/Smtp/index.tsx
+++ b/ui/src/pages/Admin/Smtp/index.tsx
@@ -224,13 +224,15 @@ const Smtp: FC = () => {
return (
<>
<h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- uiSchema={uiSchema}
- formData={formData}
- onChange={handleOnChange}
- onSubmit={onSubmit}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onChange={handleOnChange}
+ onSubmit={onSubmit}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/TagsSettings/index.tsx
b/ui/src/pages/Admin/TagsSettings/index.tsx
new file mode 100644
index 00000000..f2ab2f5b
--- /dev/null
+++ b/ui/src/pages/Admin/TagsSettings/index.tsx
@@ -0,0 +1,170 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import {
+ SchemaForm,
+ JSONSchema,
+ UISchema,
+ initFormData,
+ TabNav,
+} from '@/components';
+import { ADMIN_TAGS_NAV_MENUS } from '@/common/constants';
+import * as Type from '@/common/interface';
+import { handleFormError, scrollToElementTop } from '@/utils';
+import { writeSettingStore } from '@/stores';
+import { getAdminTagsSetting, updateAdminTagsSetting } from '@/services/admin';
+import { useToast } from '@/hooks';
+
+const QaSettings = () => {
+ const { t } = useTranslation('translation', {
+ keyPrefix: 'admin.write',
+ });
+ const Toast = useToast();
+ const schema: JSONSchema = {
+ title: t('page_title'),
+ properties: {
+ reserved_tags: {
+ type: 'string',
+ title: t('reserved_tags.label'),
+ description: t('reserved_tags.text'),
+ },
+ recommend_tags: {
+ type: 'string',
+ title: t('recommend_tags.label'),
+ description: t('recommend_tags.text'),
+ },
+ required_tag: {
+ type: 'boolean',
+ title: t('required_tag.title'),
+ description: t('required_tag.text'),
+ },
+ },
+ };
+ const uiSchema: UISchema = {
+ reserved_tags: {
+ 'ui:widget': 'tag_selector',
+ 'ui:options': {
+ label: t('reserved_tags.label'),
+ },
+ },
+ recommend_tags: {
+ 'ui:widget': 'tag_selector',
+ 'ui:options': {
+ label: t('recommend_tags.label'),
+ },
+ },
+ required_tag: {
+ 'ui:widget': 'switch',
+ 'ui:options': {
+ label: t('required_tag.label'),
+ },
+ },
+ };
+ const [formData, setFormData] = useState<Type.FormDataType>(
+ initFormData(schema),
+ );
+
+ const handleValueChange = (data: Type.FormDataType) => {
+ setFormData(data);
+ };
+
+ const checkValidated = (): boolean => {
+ let bol = true;
+ const { recommend_tags, reserved_tags } = formData;
+ // 找出 recommend_tags 和 reserved_tags 中是否有重复的标签
+ // 通过标签中的 slug_name 来去重
+ const repeatTag = recommend_tags.value.filter((tag) =>
+ reserved_tags.value.some((rTag) => rTag?.slug_name === tag?.slug_name),
+ );
+ if (repeatTag.length > 0) {
+ handleValueChange({
+ ...formData,
+ recommend_tags: {
+ ...recommend_tags,
+ errorMsg: t('recommend_tags.msg.contain_reserved'),
+ isInvalid: true,
+ },
+ });
+ bol = false;
+ const ele = document.getElementById('recommend_tags');
+ scrollToElementTop(ele);
+ } else {
+ handleValueChange({
+ ...formData,
+ recommend_tags: {
+ ...recommend_tags,
+ errorMsg: '',
+ isInvalid: false,
+ },
+ });
+ }
+ return bol;
+ };
+
+ const onSubmit = (evt) => {
+ evt.preventDefault();
+ evt.stopPropagation();
+ if (!checkValidated()) {
+ return;
+ }
+ const reqParams: Type.AdminTagsSetting = {
+ recommend_tags: formData.recommend_tags.value,
+ reserved_tags: formData.reserved_tags.value,
+ required_tag: formData.required_tag.value,
+ };
+ updateAdminTagsSetting(reqParams)
+ .then(() => {
+ Toast.onShow({
+ msg: t('update', { keyPrefix: 'toast' }),
+ variant: 'success',
+ });
+ writeSettingStore.getState().update({ ...reqParams });
+ })
+ .catch((err) => {
+ if (err.isError) {
+ const data = handleFormError(err, formData);
+ setFormData({ ...data });
+ const ele = document.getElementById(err.list[0].error_field);
+ scrollToElementTop(ele);
+ }
+ });
+ };
+
+ useEffect(() => {
+ getAdminTagsSetting().then((res) => {
+ if (res) {
+ const formMeta = { ...formData };
+ if (Array.isArray(res.recommend_tags)) {
+ formData.recommend_tags.value = res.recommend_tags;
+ } else {
+ formData.recommend_tags.value = [];
+ }
+ if (Array.isArray(res.reserved_tags)) {
+ formData.reserved_tags.value = res.reserved_tags;
+ } else {
+ formData.reserved_tags.value = [];
+ }
+ formMeta.required_tag.value = res.required_tag;
+ setFormData(formMeta);
+ }
+ });
+ }, []);
+
+ return (
+ <>
+ <h3 className="mb-4">{t('tags', { keyPrefix: 'nav_menus' })}</h3>
+ <TabNav menus={ADMIN_TAGS_NAV_MENUS} />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onChange={handleValueChange}
+ onSubmit={onSubmit}
+ />
+ </div>
+ </>
+ );
+};
+
+export default QaSettings;
diff --git a/ui/src/pages/Admin/Themes/index.tsx
b/ui/src/pages/Admin/Themes/index.tsx
index 873dd7c9..c6983cbe 100644
--- a/ui/src/pages/Admin/Themes/index.tsx
+++ b/ui/src/pages/Admin/Themes/index.tsx
@@ -205,13 +205,15 @@ const Index: FC = () => {
return (
<>
<h3 className="mb-4">{t('page_title')}</h3>
- <SchemaForm
- schema={schema}
- formData={formData}
- onSubmit={onSubmit}
- uiSchema={uiSchema}
- onChange={handleOnChange}
- />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ formData={formData}
+ onSubmit={onSubmit}
+ uiSchema={uiSchema}
+ onChange={handleOnChange}
+ />
+ </div>
</>
);
};
diff --git a/ui/src/pages/Admin/Users/index.tsx
b/ui/src/pages/Admin/Users/index.tsx
index 22042900..200aacf3 100644
--- a/ui/src/pages/Admin/Users/index.tsx
+++ b/ui/src/pages/Admin/Users/index.tsx
@@ -32,6 +32,7 @@ import {
Empty,
QueryGroup,
Modal,
+ TabNav,
} from '@/components';
import * as Type from '@/common/interface';
import { useUserModal } from '@/hooks';
@@ -45,6 +46,7 @@ import {
deletePermanently,
} from '@/services';
import { formatCount } from '@/utils';
+import { ADMIN_USERS_NAV_MENUS } from '@/common/constants';
import DeleteUserModal from './components/DeleteUserModal';
import Action from './components/Action';
@@ -208,6 +210,7 @@ const Users: FC = () => {
return (
<>
<h3 className="mb-4">{t('title')}</h3>
+ <TabNav menus={ADMIN_USERS_NAV_MENUS} />
<div className="d-flex flex-wrap justify-content-between
align-items-center">
<Stack direction="horizontal" gap={3} className="mb-3">
<QueryGroup
diff --git a/ui/src/pages/Admin/UsersSettings/index.tsx
b/ui/src/pages/Admin/UsersSettings/index.tsx
new file mode 100644
index 00000000..769c9f0a
--- /dev/null
+++ b/ui/src/pages/Admin/UsersSettings/index.tsx
@@ -0,0 +1,132 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { SchemaForm, JSONSchema, UISchema, TabNav } from '@/components';
+import {
+ ADMIN_USERS_NAV_MENUS,
+ SYSTEM_AVATAR_OPTIONS,
+} from '@/common/constants';
+import { FormDataType } from '@/common/interface';
+import { useAdminUsersSettings, updateAdminUsersSettings } from '@/services';
+import { useToast } from '@/hooks';
+import { siteInfoStore } from '@/stores';
+import { handleFormError, scrollToElementTop } from '@/utils';
+
+const UsersSettings = () => {
+ const { t } = useTranslation('translation', {
+ keyPrefix: 'admin.interface',
+ });
+ const { data: setting } = useAdminUsersSettings();
+ const Toast = useToast();
+ const schema: JSONSchema = {
+ title: t('page_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: setting?.default_avatar || 'system',
+ },
+ gravatar_base_url: {
+ type: 'string',
+ title: t('gravatar_base_url.label'),
+ description: t('gravatar_base_url.text'),
+ default: setting?.gravatar_base_url || '',
+ },
+ },
+ };
+
+ const [formData, setFormData] = useState<FormDataType>({
+ default_avatar: {
+ value: setting?.default_avatar || 'system',
+ isInvalid: false,
+ errorMsg: '',
+ },
+ gravatar_base_url: {
+ value: setting?.gravatar_base_url || '',
+ isInvalid: false,
+ errorMsg: '',
+ },
+ });
+
+ const uiSchema: UISchema = {
+ default_avatar: {
+ 'ui:widget': 'select',
+ },
+ gravatar_base_url: {
+ 'ui:widget': 'input',
+ 'ui:options': {
+ placeholder: 'https://www.gravatar.com/avatar/',
+ },
+ },
+ };
+
+ const handleValueChange = (data: FormDataType) => {
+ setFormData(data);
+ };
+
+ const onSubmit = (evt) => {
+ evt.preventDefault();
+ evt.stopPropagation();
+ const reqParams = {
+ default_avatar: formData.default_avatar.value,
+ gravatar_base_url: formData.gravatar_base_url.value,
+ };
+ updateAdminUsersSettings(reqParams)
+ .then(() => {
+ Toast.onShow({
+ msg: t('update', { keyPrefix: 'toast' }),
+ variant: 'success',
+ });
+ siteInfoStore.getState().updateUsers({
+ ...siteInfoStore.getState().users,
+ ...reqParams,
+ });
+ })
+ .catch((err) => {
+ if (err.isError) {
+ const data = handleFormError(err, formData);
+ setFormData({ ...data });
+ const ele = document.getElementById(err.list[0].error_field);
+ scrollToElementTop(ele);
+ }
+ });
+ };
+
+ useEffect(() => {
+ if (setting) {
+ const formMeta = {};
+ Object.keys(setting).forEach((k) => {
+ let v = setting[k];
+ if (k === 'default_avatar' && !v) {
+ v = 'system';
+ }
+ if (k === 'gravatar_base_url' && !v) {
+ v = '';
+ }
+ formMeta[k] = { ...formData[k], value: v };
+ });
+ setFormData({ ...formData, ...formMeta });
+ }
+ }, [setting]);
+
+ return (
+ <>
+ <h3 className="mb-4">{t('tags', { keyPrefix: 'nav_menus' })}</h3>
+ <TabNav menus={ADMIN_USERS_NAV_MENUS} />
+ <div className="max-w-748">
+ <SchemaForm
+ schema={schema}
+ uiSchema={uiSchema}
+ formData={formData}
+ onChange={handleValueChange}
+ onSubmit={onSubmit}
+ />
+ </div>
+ </>
+ );
+};
+
+export default UsersSettings;
diff --git a/ui/src/pages/Admin/Write/index.tsx
b/ui/src/pages/Admin/Write/index.tsx
deleted file mode 100644
index fee2d28c..00000000
--- a/ui/src/pages/Admin/Write/index.tsx
+++ /dev/null
@@ -1,473 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { FC, useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { Form, Button } from 'react-bootstrap';
-
-import { TagSelector } from '@/components';
-import type * as Type from '@/common/interface';
-import { useToast } from '@/hooks';
-import {
- getRequireAndReservedTag,
- postRequireAndReservedTag,
-} from '@/services';
-import { handleFormError, scrollToElementTop } from '@/utils';
-import { writeSettingStore } from '@/stores';
-
-const initFormData = {
- reserved_tags: {
- value: [] as Type.Tag[], // Replace `Type.Tag` with the correct type for
`reserved_tags.value`
- errorMsg: '',
- isInvalid: false,
- },
- min_content: {
- value: 0,
- errorMsg: '',
- isInvalid: false,
- },
- min_tags: {
- value: 0,
- errorMsg: '',
- isInvalid: false,
- },
- recommend_tags: {
- value: [] as Type.Tag[],
- errorMsg: '',
- isInvalid: false,
- },
- required_tag: {
- value: false,
- errorMsg: '',
- isInvalid: false,
- },
- restrict_answer: {
- value: false,
- errorMsg: '',
- isInvalid: false,
- },
- max_image_size: {
- value: 0,
- errorMsg: '',
- isInvalid: false,
- },
- max_attachment_size: {
- value: 0,
- errorMsg: '',
- isInvalid: false,
- },
- max_image_megapixel: {
- value: 0,
- errorMsg: '',
- isInvalid: false,
- },
- authorized_image_extensions: {
- value: '',
- errorMsg: '',
- isInvalid: false,
- },
- authorized_attachment_extensions: {
- value: '',
- errorMsg: '',
- isInvalid: false,
- },
-};
-
-const Index: FC = () => {
- const { t } = useTranslation('translation', {
- keyPrefix: 'admin.write',
- });
- const Toast = useToast();
-
- const [formData, setFormData] = useState(initFormData);
-
- const handleValueChange = (value) => {
- setFormData({
- ...formData,
- ...value,
- });
- };
-
- const checkValidated = (): boolean => {
- let bol = true;
- const { recommend_tags, reserved_tags } = formData;
- // 找出 recommend_tags 和 reserved_tags 中是否有重复的标签
- // 通过标签中的 slug_name 来去重
- const repeatTag = recommend_tags.value.filter((tag) =>
- reserved_tags.value.some((rTag) => rTag?.slug_name === tag?.slug_name),
- );
- if (repeatTag.length > 0) {
- handleValueChange({
- recommend_tags: {
- ...recommend_tags,
- errorMsg: t('recommend_tags.msg.contain_reserved'),
- isInvalid: true,
- },
- });
- bol = false;
- const ele = document.getElementById('recommend_tags');
- scrollToElementTop(ele);
- } else {
- handleValueChange({
- recommend_tags: {
- ...recommend_tags,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }
- return bol;
- };
-
- const onSubmit = (evt) => {
- evt.preventDefault();
- evt.stopPropagation();
- if (!checkValidated()) {
- return;
- }
- const reqParams: Type.AdminSettingsWrite = {
- recommend_tags: formData.recommend_tags.value,
- min_tags: Number(formData.min_tags.value),
- reserved_tags: formData.reserved_tags.value,
- required_tag: formData.required_tag.value,
- restrict_answer: formData.restrict_answer.value,
- min_content: Number(formData.min_content.value),
- max_image_size: Number(formData.max_image_size.value),
- max_attachment_size: Number(formData.max_attachment_size.value),
- max_image_megapixel: Number(formData.max_image_megapixel.value),
- authorized_image_extensions:
- formData.authorized_image_extensions.value?.length > 0
- ? formData.authorized_image_extensions.value
- .split(',')
- ?.map((item) => item.trim().toLowerCase())
- : [],
- authorized_attachment_extensions:
- formData.authorized_attachment_extensions.value?.length > 0
- ? formData.authorized_attachment_extensions.value
- .split(',')
- ?.map((item) => item.trim().toLowerCase())
- : [],
- };
- postRequireAndReservedTag(reqParams)
- .then(() => {
- Toast.onShow({
- msg: t('update', { keyPrefix: 'toast' }),
- variant: 'success',
- });
- writeSettingStore
- .getState()
- .update({ restrict_answer: reqParams.restrict_answer, ...reqParams
});
- })
- .catch((err) => {
- if (err.isError) {
- const data = handleFormError(err, formData);
- setFormData({ ...data });
- const ele = document.getElementById(err.list[0].error_field);
- scrollToElementTop(ele);
- }
- });
- };
-
- const initData = () => {
- getRequireAndReservedTag().then((res) => {
- if (Array.isArray(res.recommend_tags)) {
- formData.recommend_tags.value = res.recommend_tags;
- }
- formData.min_content.value = res.min_content;
- formData.min_tags.value = res.min_tags;
- formData.required_tag.value = res.required_tag;
- formData.restrict_answer.value = res.restrict_answer;
- if (Array.isArray(res.reserved_tags)) {
- formData.reserved_tags.value = res.reserved_tags;
- }
- formData.max_image_size.value = res.max_image_size;
- formData.max_attachment_size.value = res.max_attachment_size;
- formData.max_image_megapixel.value = res.max_image_megapixel;
- formData.authorized_image_extensions.value =
- res.authorized_image_extensions?.join(', ').toLowerCase();
- formData.authorized_attachment_extensions.value =
- res.authorized_attachment_extensions?.join(', ').toLowerCase();
- setFormData({ ...formData });
- });
- };
-
- useEffect(() => {
- initData();
- }, []);
-
- return (
- <>
- <h3 className="mb-4">{t('page_title')}</h3>
- <Form noValidate onSubmit={onSubmit}>
- <Form.Group className="mb-3" controlId="reserved_tags">
- <Form.Label>{t('reserved_tags.label')}</Form.Label>
- <TagSelector
- value={formData.reserved_tags.value}
- onChange={(val) => {
- handleValueChange({
- reserved_tags: {
- value: val,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- showRequiredTag={false}
- maxTagLength={0}
- tagStyleMode="simple"
- formText={t('reserved_tags.text')}
- isInvalid={formData.reserved_tags.isInvalid}
- errMsg={formData.reserved_tags.errorMsg}
- />
- </Form.Group>
-
- <Form.Group className="mb-3" controlId="recommend_tags">
- <Form.Label>{t('recommend_tags.label')}</Form.Label>
- <TagSelector
- value={formData.recommend_tags.value}
- onChange={(val) => {
- handleValueChange({
- recommend_tags: {
- value: val,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- showRequiredTag={false}
- tagStyleMode="simple"
- formText={t('recommend_tags.text')}
- isInvalid={formData.recommend_tags.isInvalid}
- errMsg={formData.recommend_tags.errorMsg}
- />
- </Form.Group>
- <Form.Group className="mb-3" controlId="min_tags">
- <Form.Label>{t('min_tags.label')}</Form.Label>
- <Form.Control
- type="number"
- inputMode="numeric"
- min={0}
- value={formData.min_tags.value}
- isInvalid={formData.min_tags.isInvalid}
- onChange={(evt) => {
- handleValueChange({
- min_tags: {
- value: evt.target.value,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('min_tags.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.min_tags.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
- <Form.Group className="mb-3" controlId="required_tag">
- <Form.Label>{t('required_tag.title')}</Form.Label>
- <Form.Switch
- label={t('required_tag.label')}
- checked={formData.required_tag.value}
- onChange={(evt) => {
- handleValueChange({
- required_tag: {
- value: evt.target.checked,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('required_tag.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.required_tag.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
- <Form.Group className="mb-3" controlId="min_content">
- <Form.Label>{t('min_content.label')}</Form.Label>
- <Form.Control
- type="number"
- inputMode="numeric"
- min={0}
- value={formData.min_content.value}
- isInvalid={formData.min_content.isInvalid}
- onChange={(evt) => {
- handleValueChange({
- min_content: {
- value: evt.target.value,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('min_content.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.min_content.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
- <Form.Group className="mb-3" controlId="restrict_answer">
- <Form.Label>{t('restrict_answer.title')}</Form.Label>
- <Form.Switch
- label={t('restrict_answer.label')}
- checked={formData.restrict_answer.value}
- onChange={(evt) => {
- handleValueChange({
- restrict_answer: {
- value: evt.target.checked,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('restrict_answer.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.restrict_answer.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
-
- <Form.Group className="mb-3" controlId="max_image_size">
- <Form.Label>{t('image_size.label')}</Form.Label>
- <Form.Control
- type="number"
- inputMode="numeric"
- min={0}
- value={formData.max_image_size.value}
- isInvalid={formData.max_image_size.isInvalid}
- onChange={(evt) => {
- handleValueChange({
- max_image_size: {
- value: evt.target.value,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('image_size.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.max_image_size.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
-
- <Form.Group className="mb-3" controlId="max_attachment_size">
- <Form.Label>{t('attachment_size.label')}</Form.Label>
- <Form.Control
- type="number"
- inputMode="numeric"
- min={0}
- value={formData.max_attachment_size.value}
- isInvalid={formData.max_attachment_size.isInvalid}
- onChange={(evt) => {
- handleValueChange({
- max_attachment_size: {
- value: evt.target.value,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('attachment_size.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.max_attachment_size.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
-
- <Form.Group className="mb-3" controlId="max_image_megapixel">
- <Form.Label>{t('image_megapixels.label')}</Form.Label>
- <Form.Control
- type="number"
- inputMode="numeric"
- min={0}
- isInvalid={formData.max_image_megapixel.isInvalid}
- value={formData.max_image_megapixel.value}
- onChange={(evt) => {
- handleValueChange({
- max_image_megapixel: {
- value: evt.target.value,
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('image_megapixels.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.max_image_megapixel.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
-
- <Form.Group className="mb-3" controlId="authorized_image_extensions">
- <Form.Label>{t('image_extensions.label')}</Form.Label>
- <Form.Control
- type="text"
- value={formData.authorized_image_extensions.value}
- isInvalid={formData.authorized_image_extensions.isInvalid}
- onChange={(evt) => {
- handleValueChange({
- authorized_image_extensions: {
- value: evt.target.value.toLowerCase(),
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('image_extensions.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.authorized_image_extensions.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
-
- <Form.Group
- className="mb-3"
- controlId="authorized_attachment_extensions">
- <Form.Label>{t('attachment_extensions.label')}</Form.Label>
- <Form.Control
- type="text"
- value={formData.authorized_attachment_extensions.value}
- isInvalid={formData.authorized_attachment_extensions.isInvalid}
- onChange={(evt) => {
- handleValueChange({
- authorized_attachment_extensions: {
- value: evt.target.value.toLowerCase(),
- errorMsg: '',
- isInvalid: false,
- },
- });
- }}
- />
- <Form.Text>{t('attachment_extensions.text')}</Form.Text>
- <Form.Control.Feedback type="invalid">
- {formData.authorized_attachment_extensions.errorMsg}
- </Form.Control.Feedback>
- </Form.Group>
-
- <Form.Group className="mb-3">
- <Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
- </Form.Group>
- </Form>
- </>
- );
-};
-
-export default Index;
diff --git a/ui/src/pages/Admin/index.scss b/ui/src/pages/Admin/index.scss
index 5748d053..c33e92be 100644
--- a/ui/src/pages/Admin/index.scss
+++ b/ui/src/pages/Admin/index.scss
@@ -25,6 +25,10 @@
max-width: 30rem;
}
+.max-w-748 {
+ max-width: 748px;
+}
+
@media screen and (max-width: 768px) {
.max-w-30 {
max-width: 15rem;
diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx
index 27503296..da167ac4 100644
--- a/ui/src/pages/Admin/index.tsx
+++ b/ui/src/pages/Admin/index.tsx
@@ -20,7 +20,7 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Row, Col } from 'react-bootstrap';
-import { Outlet, useMatch } from 'react-router-dom';
+import { Outlet } from 'react-router-dom';
import { usePageTags } from '@/hooks';
import { AdminSideNav, Footer } from '@/components';
@@ -28,19 +28,8 @@ import { AdminSideNav, Footer } from '@/components';
import '@/common/sideNavLayout.scss';
import './index.scss';
-const g10Paths = [
- 'dashboard',
- 'questions',
- 'answers',
- 'users',
- 'badges',
- 'flags',
- 'installed-plugins',
-];
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
- const pathMatch = useMatch('/admin/:path');
- const curPath = pathMatch?.params.path || 'dashboard';
usePageTags({
title: t('admin'),
@@ -59,9 +48,6 @@ const Index: FC = () => {
<Col className="page-main flex-auto">
<Outlet />
</Col>
- {g10Paths.find((v) => curPath === v) ? null : (
- <Col className="page-right-side" />
- )}
</Row>
</div>
</div>
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx
index bd229165..048ca812 100644
--- a/ui/src/pages/Layout/index.tsx
+++ b/ui/src/pages/Layout/index.tsx
@@ -28,7 +28,7 @@ import {
toastStore,
loginToContinueStore,
errorCodeStore,
- siteLealStore,
+ siteSecurityStore,
themeSettingStore,
} from '@/stores';
import {
@@ -49,7 +49,7 @@ const Layout: FC = () => {
const location = useLocation();
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
const externalToast = useExternalToast();
- const externalContentDisplay = siteLealStore(
+ const externalContentDisplay = siteSecurityStore(
(state) => state.external_content_display,
);
const closeToast = () => {
@@ -59,7 +59,6 @@ const Layout: FC = () => {
const { show: showLoginToContinueModal } = loginToContinueStore();
const { data: notificationData } = useQueryNotificationStatus();
const layout = themeSettingStore((state) => state.layout);
- console.log(layout);
useEffect(() => {
// handle footnote links
const fixFootnoteLinks = () => {
diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx
b/ui/src/pages/Users/Settings/Profile/index.tsx
index 7fb8247c..62bd6fb8 100644
--- a/ui/src/pages/Users/Settings/Profile/index.tsx
+++ b/ui/src/pages/Users/Settings/Profile/index.tsx
@@ -25,7 +25,7 @@ import { sha256 } from 'js-sha256';
import type { FormDataType } from '@/common/interface';
import { UploadImg, Avatar, Icon, ImgViewer } from '@/components';
-import { loggedUserInfoStore, userCenterStore, interfaceStore } from
'@/stores';
+import { loggedUserInfoStore, userCenterStore, siteInfoStore } from '@/stores';
import { useToast } from '@/hooks';
import {
modifyUserInfo,
@@ -42,7 +42,7 @@ const Index: React.FC = () => {
const toast = useToast();
const { user, update } = loggedUserInfoStore();
const { agent: ucAgent } = userCenterStore();
- const { interface: interfaceSetting } = interfaceStore();
+ const { users: usersSettings } = siteInfoStore();
const [mailHash, setMailHash] = useState('');
const [count] = useState(0);
const [profileAgent, setProfileAgent] = useState<UcSettingAgent>();
@@ -384,7 +384,7 @@ const Index: React.FC = () => {
<span>{t('avatar.gravatar_text')}</span>
<a
href={
- interfaceSetting.gravatar_base_url.includes(
+ usersSettings.gravatar_base_url.includes(
'gravatar.cn',
)
? 'https://gravatar.cn'
@@ -393,9 +393,7 @@ const Index: React.FC = () => {
className="ms-1"
target="_blank"
rel="noreferrer">
- {interfaceSetting.gravatar_base_url.includes(
- 'gravatar.cn',
- )
+
{usersSettings.gravatar_base_url.includes('gravatar.cn')
? 'gravatar.cn'
: 'gravatar.com'}
</a>
diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts
index 3ca8431f..7d7003a1 100644
--- a/ui/src/router/routes.ts
+++ b/ui/src/router/routes.ts
@@ -358,9 +358,25 @@ const routes: RouteNode[] = [
page: 'pages/Admin/Dashboard',
},
{
- path: 'answers',
+ path: 'qa/questions',
+ page: 'pages/Admin/Questions',
+ },
+ {
+ path: 'qa/answers',
page: 'pages/Admin/Answers',
},
+ {
+ path: 'qa/settings',
+ page: 'pages/Admin/QaSettings',
+ },
+ {
+ path: 'tags/settings',
+ page: 'pages/Admin/TagsSettings',
+ },
+ {
+ path: 'security',
+ page: 'pages/Admin/Security',
+ },
{
path: 'themes',
page: 'pages/Admin/Themes',
@@ -377,14 +393,14 @@ const routes: RouteNode[] = [
path: 'interface',
page: 'pages/Admin/Interface',
},
- {
- path: 'questions',
- page: 'pages/Admin/Questions',
- },
{
path: 'users',
page: 'pages/Admin/Users',
},
+ {
+ path: 'users/settings',
+ page: 'pages/Admin/UsersSettings',
+ },
{
path: 'users/:user_id',
page: 'pages/Admin/UserOverview',
@@ -398,12 +414,12 @@ const routes: RouteNode[] = [
page: 'pages/Admin/Branding',
},
{
- path: 'legal',
- page: 'pages/Admin/Legal',
+ path: 'rules/policies',
+ page: 'pages/Admin/Policies',
},
{
- path: 'write',
- page: 'pages/Admin/Write',
+ path: 'files',
+ page: 'pages/Admin/Files',
},
{
path: 'seo',
@@ -414,7 +430,7 @@ const routes: RouteNode[] = [
page: 'pages/Admin/Login',
},
{
- path: 'privileges',
+ path: 'rules/privileges',
page: 'pages/Admin/Privileges',
},
{
diff --git a/ui/src/services/admin/index.ts b/ui/src/services/admin/index.ts
index af83d365..3fa211ee 100644
--- a/ui/src/services/admin/index.ts
+++ b/ui/src/services/admin/index.ts
@@ -25,3 +25,4 @@ export * from './users';
export * from './dashboard';
export * from './plugins';
export * from './badges';
+export * from './tags';
diff --git a/ui/src/services/admin/question.ts
b/ui/src/services/admin/question.ts
index 670534e2..bcd3ac7f 100644
--- a/ui/src/services/admin/question.ts
+++ b/ui/src/services/admin/question.ts
@@ -46,3 +46,13 @@ export const changeQuestionStatus = (
status,
});
};
+
+export const getQuestionSetting = () => {
+ return request.get<Type.AdminQuestionSetting>(
+ '/answer/admin/api/siteinfo/question',
+ );
+};
+
+export const updateQuestionSetting = (params: Type.AdminQuestionSetting) => {
+ return request.put('/answer/admin/api/siteinfo/question', params);
+};
diff --git a/ui/src/services/admin/settings.ts
b/ui/src/services/admin/settings.ts
index f2b9e598..c1f99d40 100644
--- a/ui/src/services/admin/settings.ts
+++ b/ui/src/services/admin/settings.ts
@@ -122,22 +122,12 @@ export const brandSetting = (params:
Type.AdminSettingBranding) => {
return request.put('/answer/admin/api/siteinfo/branding', params);
};
-export const getRequireAndReservedTag = () => {
- return request.get('/answer/admin/api/siteinfo/write');
+export const getAdminFilesSetting = () => {
+ return request.get('/answer/admin/api/siteinfo/advanced');
};
-export const postRequireAndReservedTag = (params) => {
- return request.put('/answer/admin/api/siteinfo/write', params);
-};
-
-export const getLegalSetting = () => {
- return request.get<Type.AdminSettingsLegal>(
- '/answer/admin/api/siteinfo/legal',
- );
-};
-
-export const putLegalSetting = (params: Type.AdminSettingsLegal) => {
- return request.put('/answer/admin/api/siteinfo/legal', params);
+export const updateAdminFilesSetting = (params: Type.AdminSettingsWrite) => {
+ return request.put('/answer/admin/api/siteinfo/advanced', params);
};
export const getSeoSetting = () => {
@@ -195,3 +185,23 @@ export const getPrivilegeSetting = () => {
export const putPrivilegeSetting = (params: AdminSettingsPrivilegeReq) => {
return request.put('/answer/admin/api/setting/privileges', params);
};
+
+export const getPoliciesSetting = () => {
+ return request.get<Type.AdminSettingsLegal>(
+ '/answer/admin/api/siteinfo/polices',
+ );
+};
+
+export const putPoliciesSetting = (params: Type.AdminSettingsLegal) => {
+ return request.put('/answer/admin/api/siteinfo/polices', params);
+};
+
+export const getSecuritySetting = () => {
+ return request.get<Type.AdminSettingsSecurity>(
+ '/answer/admin/api/siteinfo/security',
+ );
+};
+
+export const putSecuritySetting = (params: Type.AdminSettingsSecurity) => {
+ return request.put('/answer/admin/api/siteinfo/security', params);
+};
diff --git a/ui/src/pages/Admin/index.scss b/ui/src/services/admin/tags.ts
similarity index 69%
copy from ui/src/pages/Admin/index.scss
copy to ui/src/services/admin/tags.ts
index 5748d053..c7d6bc77 100644
--- a/ui/src/pages/Admin/index.scss
+++ b/ui/src/services/admin/tags.ts
@@ -17,20 +17,13 @@
* under the License.
*/
-.min-w-15 {
- min-width: 15rem;
-}
+import request from '@/utils/request';
+import type * as Type from '@/common/interface';
-.max-w-30 {
- max-width: 30rem;
-}
+export const getAdminTagsSetting = () => {
+ return request.get<Type.AdminTagsSetting>('/answer/admin/api/siteinfo/tag');
+};
-@media screen and (max-width: 768px) {
- .max-w-30 {
- max-width: 15rem;
- }
-}
-
-.table tr th {
- white-space: nowrap;
-}
+export const updateAdminTagsSetting = (params: Type.AdminTagsSetting) => {
+ return request.put('/answer/admin/api/siteinfo/tag', params);
+};
diff --git a/ui/src/services/admin/users.ts b/ui/src/services/admin/users.ts
index ee8cad5d..c7aeafe5 100644
--- a/ui/src/services/admin/users.ts
+++ b/ui/src/services/admin/users.ts
@@ -94,3 +94,22 @@ export const postUserActivation = (userId: string) => {
user_id: userId,
});
};
+
+export const useAdminUsersSettings = () => {
+ const apiUrl = `/answer/admin/api/siteinfo/users-settings`;
+ const { data, error } = useSWR<
+ {
+ default_avatar: string;
+ gravatar_base_url: string;
+ },
+ Error
+ >(apiUrl, request.instance.get);
+ return { data, isLoading: !data && !error, error };
+};
+
+export const updateAdminUsersSettings = (params: {
+ default_avatar: string;
+ gravatar_base_url: string;
+}) => {
+ return request.put('/answer/admin/api/siteinfo/users-settings', params);
+};
diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts
index 66d59b32..1bd2fece 100644
--- a/ui/src/stores/index.ts
+++ b/ui/src/stores/index.ts
@@ -33,7 +33,7 @@ import loginToContinueStore from './loginToContinue';
import errorCodeStore from './errorCode';
import sideNavStore from './sideNav';
import commentReplyStore from './commentReply';
-import siteLealStore from './siteLegal';
+import siteSecurityStore from './siteSecurity';
export {
toastStore,
@@ -52,5 +52,5 @@ export {
sideNavStore,
commentReplyStore,
writeSettingStore,
- siteLealStore,
+ siteSecurityStore,
};
diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts
index cceb425c..fdb4ab96 100644
--- a/ui/src/stores/interface.ts
+++ b/ui/src/stores/interface.ts
@@ -35,9 +35,9 @@ const interfaceSetting = create<InterfaceType>((set) => ({
gravatar_base_url: '',
},
update: (params) =>
- set(() => {
+ set((state) => {
return {
- interface: params,
+ interface: { ...state.interface, ...params },
};
}),
}));
diff --git a/ui/src/stores/loginSetting.ts b/ui/src/stores/loginSetting.ts
index 73fd4880..7acf765e 100644
--- a/ui/src/stores/loginSetting.ts
+++ b/ui/src/stores/loginSetting.ts
@@ -29,7 +29,6 @@ interface IType {
const loginSetting = create<IType>((set) => ({
login: {
allow_new_registrations: true,
- login_required: false,
allow_email_registrations: true,
allow_email_domains: [],
allow_password_login: true,
diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts
index 725546d6..c529cc62 100644
--- a/ui/src/stores/siteInfo.ts
+++ b/ui/src/stores/siteInfo.ts
@@ -39,6 +39,8 @@ const defaultUsersConf: AdminSettingsUsers = {
allow_update_location: false,
allow_update_username: false,
allow_update_website: false,
+ default_avatar: 'system',
+ gravatar_base_url: '',
};
const siteInfo = create<SiteInfoType>((set) => ({
@@ -48,7 +50,6 @@ const siteInfo = create<SiteInfoType>((set) => ({
short_description: '',
site_url: '',
contact_email: '',
- check_update: true,
permalink: 1,
},
users: defaultUsersConf,
diff --git a/ui/src/stores/siteLegal.ts b/ui/src/stores/siteSecurity.ts
similarity index 75%
rename from ui/src/stores/siteLegal.ts
rename to ui/src/stores/siteSecurity.ts
index 29a26ea3..e4e5bb52 100644
--- a/ui/src/stores/siteLegal.ts
+++ b/ui/src/stores/siteSecurity.ts
@@ -19,12 +19,20 @@
import { create } from 'zustand';
-interface LealStore {
+interface SecurityStore {
+ login_required: boolean;
+ check_update: boolean;
external_content_display: string;
- update: (params: { external_content_display: string }) => void;
+ update: (params: {
+ external_content_display: string;
+ check_update: boolean;
+ login_required: boolean;
+ }) => void;
}
-const siteLealStore = create<LealStore>((set) => ({
+const siteSecurityStore = create<SecurityStore>((set) => ({
+ login_required: false,
+ check_update: true,
external_content_display: 'always_display',
update: (params) =>
set((state) => {
@@ -35,4 +43,4 @@ const siteLealStore = create<LealStore>((set) => ({
}),
}));
-export default siteLealStore;
+export default siteSecurityStore;
diff --git a/ui/src/stores/writeSetting.ts b/ui/src/stores/writeSetting.ts
index 9f6542d2..576979a2 100644
--- a/ui/src/stores/writeSetting.ts
+++ b/ui/src/stores/writeSetting.ts
@@ -19,11 +19,17 @@
import { create } from 'zustand';
-import { AdminSettingsWrite } from '@/common/interface';
+import {
+ AdminSettingsWrite,
+ AdminQuestionSetting,
+ AdminTagsSetting,
+} from '@/common/interface';
interface IProps {
- write: AdminSettingsWrite;
- update: (params: AdminSettingsWrite) => void;
+ write: AdminSettingsWrite & AdminQuestionSetting & AdminTagsSetting;
+ update: (
+ params: AdminSettingsWrite | AdminQuestionSetting | AdminTagsSetting,
+ ) => void;
}
const Index = create<IProps>((set) => ({
diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts
index 24064669..6e6a6d80 100644
--- a/ui/src/utils/guard.ts
+++ b/ui/src/utils/guard.ts
@@ -30,7 +30,7 @@ import {
loginToContinueStore,
pageTagStore,
writeSettingStore,
- siteLealStore,
+ siteSecurityStore,
} from '@/stores';
import { RouteAlias } from '@/router/alias';
import {
@@ -263,8 +263,8 @@ export const singUpAgent = () => {
export const shouldLoginRequired = () => {
const gr: TGuardResult = { ok: true };
- const loginSetting = loginSettingStore.getState().login;
- if (!loginSetting.login_required) {
+ const { login_required } = siteSecurityStore.getState();
+ if (!login_required) {
return gr;
}
const us = deriveLoginState();
@@ -382,12 +382,11 @@ export const initAppSettingsStore = async () => {
themeSettingStore.getState().update(appSettings.theme);
seoSettingStore.getState().update(appSettings.site_seo);
writeSettingStore.getState().update({
- restrict_answer: appSettings.site_write.restrict_answer,
- ...appSettings.site_write,
- });
- siteLealStore.getState().update({
- external_content_display:
appSettings.site_legal.external_content_display,
+ ...appSettings.site_advanced,
+ ...appSettings.site_questions,
+ ...appSettings.site_tags,
});
+ siteSecurityStore.getState().update(appSettings.site_security);
}
};