You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by sh...@apache.org on 2026/01/22 04:23:00 UTC
(answer) branch ui/admin created (now 0a5c4494)
This is an automated email from the ASF dual-hosted git repository.
shuai pushed a change to branch ui/admin
in repository https://gitbox.apache.org/repos/asf/answer.git
at 0a5c4494 feat: Management background navigation machine classification adjustment, regardless of functional split or reorganization
This branch includes the following new commits:
new 0a5c4494 feat: Management background navigation machine classification adjustment, regardless of functional split or reorganization
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(answer) 01/01: feat: Management background navigation machine classification adjustment, regardless of functional split or reorganization
Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
shuai pushed a commit to branch ui/admin
in repository https://gitbox.apache.org/repos/asf/answer.git
commit 0a5c44941bb6ca54333f15d6cab8697dfdaa9b2c
Author: shuai <li...@sifou.com>
AuthorDate: Thu Jan 22 12:17:24 2026 +0800
feat: Management background navigation machine classification adjustment, 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);
}
};