@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +1 -0
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/authRedirect.d.ts +5 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +3 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +8 -1
- package/es/index.mjs +190 -141
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +9 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +17 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +195 -146
- package/lib/locale/languageCodes.js +3 -1
- package/package.json +9 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/authRedirect.test.ts +137 -0
- package/src/__tests__/browserChecker.test.ts +185 -6
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +38 -37
- package/src/__tests__/settings-layout-root.test.tsx +260 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-shell.test.tsx +200 -0
- package/src/acl/ACLProvider.tsx +21 -0
- package/src/authRedirect.ts +72 -3
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
- package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +2 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +111 -16
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +78 -32
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +3 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +14 -1
- package/src/locale/languageCodes.ts +3 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +110 -70
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +238 -0
- package/src/settings-app/SettingsShell.tsx +167 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +104 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +301 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +122 -141
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -7,29 +7,25 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ApiOutlined } from '@ant-design/icons';
|
|
11
10
|
import { PageHeader } from '@ant-design/pro-layout';
|
|
12
11
|
import { css } from '@emotion/css';
|
|
13
12
|
import { FlowModelRenderer, useFlowEngine } from '@nocobase/flow-engine';
|
|
14
|
-
import { Layout, Menu, Result, theme } from 'antd';
|
|
13
|
+
import { Layout, Menu, Result, Tabs, theme } from 'antd';
|
|
15
14
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
16
15
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import { Navigate, Outlet, useLocation, useNavigate
|
|
18
|
-
import {
|
|
19
|
-
import { ADMIN_SETTINGS_PATH, type PluginSettingsPageType } from '../PluginSettingsManager';
|
|
16
|
+
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
17
|
+
import type { PluginSettingsPageType } from '../PluginSettingsManager';
|
|
20
18
|
import { useApp } from '../hooks/useApp';
|
|
21
19
|
import { AdminSettingsLayoutModel } from './AdminSettingsLayoutModel';
|
|
20
|
+
import { useSettingsGroups } from './useSettingsGroups';
|
|
22
21
|
import {
|
|
23
22
|
ADMIN_SETTINGS_LAYOUT_MODEL_UID,
|
|
24
23
|
createSettingsPathMap,
|
|
25
|
-
|
|
26
|
-
filterVisibleSettings,
|
|
24
|
+
findSettingsByName,
|
|
27
25
|
getDefaultSettingsPath,
|
|
28
|
-
|
|
26
|
+
getSidebarMenuItems,
|
|
27
|
+
getSidebarSelectedKey,
|
|
29
28
|
matchSettingsRoute,
|
|
30
|
-
PLUGIN_MANAGER_SETTING_NAME,
|
|
31
|
-
replaceRouteParams,
|
|
32
|
-
sortTopLevelSettings,
|
|
33
29
|
} from './utils';
|
|
34
30
|
|
|
35
31
|
function SettingsEmpty(props: { type: 'home' | 'route' }) {
|
|
@@ -60,57 +56,27 @@ function SettingsEmpty(props: { type: 'home' | 'route' }) {
|
|
|
60
56
|
/**
|
|
61
57
|
* `client-v2` 的 settings 页面壳实现。
|
|
62
58
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
59
|
+
* 一级分组铺在顶栏(见 `SettingsGroupNav`),这里负责当前分组下的二三级嵌套菜单、
|
|
60
|
+
* 默认落点和两类空态,页面内容本身继续由各个 settings route 的 `<Outlet />` 渲染。
|
|
65
61
|
*/
|
|
66
62
|
export const InternalAdminSettingsLayout = () => {
|
|
67
63
|
const app = useApp();
|
|
68
64
|
const navigate = useNavigate();
|
|
69
65
|
const location = useLocation();
|
|
70
|
-
const params = useParams();
|
|
71
66
|
const { token } = theme.useToken();
|
|
72
|
-
const {
|
|
73
|
-
|
|
67
|
+
const {
|
|
68
|
+
activeGroupSettings,
|
|
69
|
+
allSettings,
|
|
70
|
+
currentSetting,
|
|
71
|
+
currentTopLevelSetting,
|
|
72
|
+
visibleSettings: allVisibleSettings,
|
|
73
|
+
} = useSettingsGroups();
|
|
74
74
|
|
|
75
|
-
const allSettings = useMemo(
|
|
76
|
-
() => filterRenderableSettings(app.pluginSettingsManager.getList(false) as PluginSettingsPageType[]),
|
|
77
|
-
[app.pluginSettingsManager],
|
|
78
|
-
);
|
|
79
|
-
const visibleSettings = useMemo(
|
|
80
|
-
() =>
|
|
81
|
-
filterVisibleSettings(
|
|
82
|
-
filterRenderableSettings(app.pluginSettingsManager.getList(true) as PluginSettingsPageType[]),
|
|
83
|
-
),
|
|
84
|
-
[app.pluginSettingsManager],
|
|
85
|
-
);
|
|
86
|
-
const pluginManagerSetting = useMemo(
|
|
87
|
-
() => visibleSettings.find((item) => item.name === PLUGIN_MANAGER_SETTING_NAME) || null,
|
|
88
|
-
[visibleSettings],
|
|
89
|
-
);
|
|
90
|
-
const normalSettings = useMemo(
|
|
91
|
-
() => sortTopLevelSettings(visibleSettings.filter((item) => item.name !== PLUGIN_MANAGER_SETTING_NAME)),
|
|
92
|
-
[visibleSettings],
|
|
93
|
-
);
|
|
94
|
-
const allVisibleSettings = useMemo(
|
|
95
|
-
() => (pluginManagerSetting ? [pluginManagerSetting, ...normalSettings] : normalSettings),
|
|
96
|
-
[normalSettings, pluginManagerSetting],
|
|
97
|
-
);
|
|
98
|
-
const registeredSettingsMapByPath = useMemo(() => createSettingsPathMap(allSettings), [allSettings]);
|
|
99
75
|
const visibleSettingsMapByPath = useMemo(() => createSettingsPathMap(allVisibleSettings), [allVisibleSettings]);
|
|
100
|
-
const currentSetting = useMemo(
|
|
101
|
-
() => matchSettingsRoute(registeredSettingsMapByPath, location.pathname),
|
|
102
|
-
[location.pathname, registeredSettingsMapByPath],
|
|
103
|
-
);
|
|
104
76
|
const currentVisibleSetting = useMemo(
|
|
105
77
|
() => matchSettingsRoute(visibleSettingsMapByPath, location.pathname),
|
|
106
78
|
[location.pathname, visibleSettingsMapByPath],
|
|
107
79
|
);
|
|
108
|
-
const currentTopLevelSetting = useMemo(() => {
|
|
109
|
-
if (!currentSetting) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
return allSettings.find((item) => item.name === currentSetting.topLevelName) || currentSetting;
|
|
113
|
-
}, [allSettings, currentSetting]);
|
|
114
80
|
const currentVisibleTopLevelSetting = useMemo(() => {
|
|
115
81
|
if (!currentSetting) {
|
|
116
82
|
return null;
|
|
@@ -118,10 +84,42 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
118
84
|
return allVisibleSettings.find((item) => item.name === currentSetting.topLevelName) || null;
|
|
119
85
|
}, [allVisibleSettings, currentSetting]);
|
|
120
86
|
const defaultSettingsPath = useMemo(() => getDefaultSettingsPath(allVisibleSettings), [allVisibleSettings]);
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
87
|
+
const settingsRootPath = app.pluginSettingsManager.getRoutePath('');
|
|
88
|
+
const settingsRootPathWithoutTrailingSlash = settingsRootPath.replace(/\/$/, '');
|
|
89
|
+
|
|
90
|
+
const sidebarMenus = useMemo(() => getSidebarMenuItems(activeGroupSettings), [activeGroupSettings]);
|
|
91
|
+
// 分组里只有一个顶级配置项时不铺左栏:那样整条侧栏只是把顶栏那一项重复一遍。
|
|
92
|
+
// 它自己的下级(用户和权限的 用户 / 角色和权限 / 同步,AI 员工的几个页面)改用页头下的 Tab。
|
|
93
|
+
const shouldShowSidebar = activeGroupSettings.length > 1;
|
|
94
|
+
// 子页面一律走页头下的 Tab:左栏只表达「哪个模块」,模块内部的分页交给 Tab,
|
|
95
|
+
// 和 v1 设置中心保持一致。
|
|
96
|
+
const pageTabs = useMemo(() => {
|
|
97
|
+
const children = (currentVisibleTopLevelSetting?.children || []).filter(
|
|
98
|
+
(item) => !item.hidden && !item.link && item.path,
|
|
99
|
+
);
|
|
100
|
+
return children.length > 1 ? children.map((item) => ({ key: item.path, label: item.label ?? item.title })) : [];
|
|
101
|
+
}, [currentVisibleTopLevelSetting]);
|
|
102
|
+
const activeTabKey = useMemo(() => {
|
|
103
|
+
if (!pageTabs.length) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const matched = pageTabs.find(
|
|
107
|
+
(tab) => location.pathname === tab.key || location.pathname.startsWith(`${tab.key}/`),
|
|
108
|
+
);
|
|
109
|
+
return matched?.key ?? pageTabs[0]?.key;
|
|
110
|
+
}, [location.pathname, pageTabs]);
|
|
111
|
+
// 命中的可能是被折叠掉的子项,要换算成左栏里真实存在的那一级,否则整个左栏都不高亮。
|
|
112
|
+
const selectedMenuKey = useMemo(
|
|
113
|
+
() =>
|
|
114
|
+
getSidebarSelectedKey(activeGroupSettings, currentVisibleSetting?.name) ||
|
|
115
|
+
getSidebarSelectedKey(activeGroupSettings, currentVisibleTopLevelSetting?.name),
|
|
116
|
+
[activeGroupSettings, currentVisibleSetting?.name, currentVisibleTopLevelSetting?.name],
|
|
117
|
+
);
|
|
118
|
+
// 页头只需要补一句「当前在哪个子页」。
|
|
119
|
+
const pageSubTitle =
|
|
120
|
+
currentVisibleSetting && currentVisibleSetting.title !== currentTopLevelSetting?.title
|
|
121
|
+
? currentVisibleSetting.title
|
|
122
|
+
: undefined;
|
|
125
123
|
|
|
126
124
|
useEffect(() => {
|
|
127
125
|
const nextTitle =
|
|
@@ -134,33 +132,10 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
134
132
|
}
|
|
135
133
|
}, [currentTopLevelSetting]);
|
|
136
134
|
|
|
137
|
-
const sidebarMenus = useMemo(() => {
|
|
138
|
-
const items: any[] = [];
|
|
139
|
-
|
|
140
|
-
if (pluginManagerSetting && snippets.includes('pm')) {
|
|
141
|
-
items.push({
|
|
142
|
-
key: pluginManagerSetting.name,
|
|
143
|
-
icon: pluginManagerSetting.icon || <ApiOutlined />,
|
|
144
|
-
label: pluginManagerSetting.label || t('Plugin manager'),
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (items.length && normalSettings.length) {
|
|
149
|
-
items.push({ type: 'divider' });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const normalMenuItems =
|
|
153
|
-
getMenuItems(normalSettings.map((item) => ({ ...item, children: undefined }) as PluginSettingsPageType)) || [];
|
|
154
|
-
|
|
155
|
-
items.push(...normalMenuItems);
|
|
156
|
-
|
|
157
|
-
return items;
|
|
158
|
-
}, [normalSettings, pluginManagerSetting, snippets, t]);
|
|
159
|
-
|
|
160
135
|
const shouldRedirectToDefault =
|
|
161
|
-
location.pathname ===
|
|
162
|
-
location.pathname ===
|
|
163
|
-
location.pathname === `${
|
|
136
|
+
location.pathname === settingsRootPath ||
|
|
137
|
+
location.pathname === settingsRootPathWithoutTrailingSlash ||
|
|
138
|
+
location.pathname === `${settingsRootPath}index`;
|
|
164
139
|
|
|
165
140
|
if (shouldRedirectToDefault && defaultSettingsPath) {
|
|
166
141
|
return <Navigate replace to={defaultSettingsPath} />;
|
|
@@ -204,41 +179,43 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
204
179
|
overflow: 'hidden',
|
|
205
180
|
}}
|
|
206
181
|
>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
>
|
|
217
|
-
<Menu
|
|
218
|
-
selectedKeys={currentVisibleTopLevelSetting?.name ? [currentVisibleTopLevelSetting.name] : []}
|
|
219
|
-
style={{ height: '100%', borderInlineEnd: 'none' }}
|
|
220
|
-
onClick={({ key }) => {
|
|
221
|
-
const topLevelSetting = allVisibleSettings.find((item) => item.name === key);
|
|
222
|
-
if (!topLevelSetting) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (topLevelSetting.link) {
|
|
227
|
-
window.open(topLevelSetting.link, '_blank', 'noopener,noreferrer');
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const firstPath = topLevelSetting.children?.length
|
|
232
|
-
? getDefaultSettingsPath(topLevelSetting.children)
|
|
233
|
-
: topLevelSetting.path;
|
|
234
|
-
|
|
235
|
-
if (firstPath) {
|
|
236
|
-
navigate(firstPath);
|
|
237
|
-
}
|
|
182
|
+
{shouldShowSidebar ? (
|
|
183
|
+
<Layout.Sider
|
|
184
|
+
width={200}
|
|
185
|
+
style={{
|
|
186
|
+
background: token.colorBgContainer,
|
|
187
|
+
borderInlineEnd: `${token.lineWidth}px solid ${token.colorBorderSecondary}`,
|
|
188
|
+
minHeight: 0,
|
|
189
|
+
overflowY: 'auto',
|
|
190
|
+
overflowX: 'hidden',
|
|
238
191
|
}}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
192
|
+
>
|
|
193
|
+
<Menu
|
|
194
|
+
mode="inline"
|
|
195
|
+
inlineIndent={16}
|
|
196
|
+
selectedKeys={selectedMenuKey ? [selectedMenuKey] : []}
|
|
197
|
+
style={{ height: '100%', borderInlineEnd: 'none' }}
|
|
198
|
+
onClick={({ key }) => {
|
|
199
|
+
const setting = findSettingsByName(activeGroupSettings, String(key));
|
|
200
|
+
if (!setting) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (setting.link) {
|
|
205
|
+
window.open(setting.link, '_blank', 'noopener,noreferrer');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const targetPath = setting.children?.length ? getDefaultSettingsPath(setting.children) : setting.path;
|
|
210
|
+
|
|
211
|
+
if (targetPath && targetPath !== location.pathname) {
|
|
212
|
+
navigate(targetPath);
|
|
213
|
+
}
|
|
214
|
+
}}
|
|
215
|
+
items={sidebarMenus}
|
|
216
|
+
/>
|
|
217
|
+
</Layout.Sider>
|
|
218
|
+
) : null}
|
|
242
219
|
<Layout.Content
|
|
243
220
|
style={{
|
|
244
221
|
background: token.colorBgLayout,
|
|
@@ -250,41 +227,45 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
250
227
|
overflow: 'hidden',
|
|
251
228
|
}}
|
|
252
229
|
>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
background: token.colorBgContainer,
|
|
258
|
-
borderBlockEnd: `${token.lineWidth}px solid ${token.colorBorderSecondary}`,
|
|
259
|
-
paddingBottom: shouldShowTabs ? 0 : token.padding,
|
|
260
|
-
}}
|
|
261
|
-
footer={
|
|
262
|
-
shouldShowTabs ? (
|
|
263
|
-
<Menu
|
|
264
|
-
mode="horizontal"
|
|
265
|
-
selectedKeys={[currentVisibleSetting?.name || currentSetting?.name]}
|
|
266
|
-
items={getMenuItems(currentVisibleTabs)}
|
|
267
|
-
onClick={({ key }) => {
|
|
268
|
-
const targetPath = replaceRouteParams(app.pluginSettingsManager.getRoutePath(String(key)), params);
|
|
269
|
-
if (location.pathname !== targetPath) {
|
|
270
|
-
navigate(targetPath);
|
|
271
|
-
}
|
|
272
|
-
}}
|
|
273
|
-
style={{ marginLeft: -token.marginSM, borderBottom: 'none' }}
|
|
274
|
-
/>
|
|
275
|
-
) : null
|
|
276
|
-
}
|
|
277
|
-
/>
|
|
230
|
+
{/*
|
|
231
|
+
页头放在滚动容器**内部**:钉在外面会让页面出现嵌套滚动(窗口一条、内容区一条),
|
|
232
|
+
插件页面自己再带滚动区时会套成两三层,而且长页面白白少掉一条页头的高度。
|
|
233
|
+
*/}
|
|
278
234
|
<div
|
|
279
235
|
style={{
|
|
280
236
|
flex: 1,
|
|
281
237
|
minHeight: 0,
|
|
282
238
|
boxSizing: 'border-box',
|
|
283
239
|
overflow: 'auto',
|
|
284
|
-
padding: token.paddingLG,
|
|
285
240
|
}}
|
|
286
241
|
>
|
|
287
|
-
<
|
|
242
|
+
<PageHeader
|
|
243
|
+
ghost={false}
|
|
244
|
+
title={currentTopLevelSetting?.title}
|
|
245
|
+
subTitle={pageTabs.length ? undefined : pageSubTitle}
|
|
246
|
+
footer={
|
|
247
|
+
pageTabs.length ? (
|
|
248
|
+
<Tabs
|
|
249
|
+
activeKey={activeTabKey}
|
|
250
|
+
items={pageTabs}
|
|
251
|
+
tabBarStyle={{ marginBottom: 0 }}
|
|
252
|
+
onChange={(key) => {
|
|
253
|
+
if (key !== location.pathname) {
|
|
254
|
+
navigate(key);
|
|
255
|
+
}
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
) : undefined
|
|
259
|
+
}
|
|
260
|
+
style={{
|
|
261
|
+
background: token.colorBgContainer,
|
|
262
|
+
borderBlockEnd: `${token.lineWidth}px solid ${token.colorBorderSecondary}`,
|
|
263
|
+
paddingBottom: pageTabs.length ? 0 : token.padding,
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
<div style={{ padding: token.paddingLG }}>
|
|
267
|
+
<Outlet />
|
|
268
|
+
</div>
|
|
288
269
|
</div>
|
|
289
270
|
</Layout.Content>
|
|
290
271
|
</Layout>
|
|
@@ -7,29 +7,14 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { Alert, Button, Form, Input, Select, Space, Upload, message, theme } from 'antd';
|
|
12
|
-
import type { RcFile, UploadRequestOption } from 'rc-upload/lib/interface';
|
|
13
|
-
import mime from 'mime';
|
|
14
|
-
import match from 'mime-match';
|
|
10
|
+
import { Alert, App, Button, Form, Input, Select, theme } from 'antd';
|
|
15
11
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
16
12
|
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import { AttachmentUpload } from '../components/AttachmentUpload';
|
|
17
14
|
import languageCodes from '../locale/languageCodes';
|
|
18
15
|
import { useApp } from '../hooks/useApp';
|
|
19
16
|
import { useSystemSettings } from '../flow/system-settings';
|
|
20
17
|
|
|
21
|
-
type UploadedAttachment = {
|
|
22
|
-
id?: number;
|
|
23
|
-
title?: string;
|
|
24
|
-
filename?: string;
|
|
25
|
-
url?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type AttachmentStorageRules = {
|
|
29
|
-
size?: number;
|
|
30
|
-
mimetype?: string | string[];
|
|
31
|
-
};
|
|
32
|
-
|
|
33
18
|
/**
|
|
34
19
|
* `System settings` 的最小 v2 表单页面。
|
|
35
20
|
*
|
|
@@ -39,14 +24,11 @@ type AttachmentStorageRules = {
|
|
|
39
24
|
export const SystemSettingsPage = () => {
|
|
40
25
|
const app = useApp();
|
|
41
26
|
const { token } = theme.useToken();
|
|
27
|
+
const { message } = App.useApp();
|
|
42
28
|
const { t } = useTranslation();
|
|
43
29
|
const { data, loading, error, mutate } = useSystemSettings();
|
|
44
30
|
const [form] = Form.useForm();
|
|
45
31
|
const [submitting, setSubmitting] = useState(false);
|
|
46
|
-
const [uploading, setUploading] = useState(false);
|
|
47
|
-
const [storageRules, setStorageRules] = useState<AttachmentStorageRules | null>(null);
|
|
48
|
-
const logo = Form.useWatch<UploadedAttachment>('logo', form);
|
|
49
|
-
|
|
50
32
|
useEffect(() => {
|
|
51
33
|
const settings = data?.data;
|
|
52
34
|
if (!settings) {
|
|
@@ -60,45 +42,6 @@ export const SystemSettingsPage = () => {
|
|
|
60
42
|
});
|
|
61
43
|
}, [data?.data, form]);
|
|
62
44
|
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
let mounted = true;
|
|
65
|
-
|
|
66
|
-
const loadStorageRules = async () => {
|
|
67
|
-
try {
|
|
68
|
-
const attachmentsCollection = app.dataSourceManager?.getCollection?.('main', 'attachments');
|
|
69
|
-
const storageName = attachmentsCollection?.getOption?.('storage');
|
|
70
|
-
|
|
71
|
-
if (!storageName) {
|
|
72
|
-
if (mounted) {
|
|
73
|
-
setStorageRules(null);
|
|
74
|
-
}
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const response = await app.apiClient.request({
|
|
79
|
-
url: `storages:getBasicInfo/${storageName}`,
|
|
80
|
-
skipNotify: true,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (!mounted) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
setStorageRules(response?.data?.data?.rules || null);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (mounted) {
|
|
90
|
-
setStorageRules(null);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
void loadStorageRules();
|
|
96
|
-
|
|
97
|
-
return () => {
|
|
98
|
-
mounted = false;
|
|
99
|
-
};
|
|
100
|
-
}, [app]);
|
|
101
|
-
|
|
102
45
|
const languageOptions = useMemo(() => {
|
|
103
46
|
const currentValues = form.getFieldValue('enabledLanguages') || data?.data?.enabledLanguages || [];
|
|
104
47
|
const supported = Array.from(new Set([...Object.keys(languageCodes), ...currentValues]));
|
|
@@ -109,78 +52,6 @@ export const SystemSettingsPage = () => {
|
|
|
109
52
|
}));
|
|
110
53
|
}, [data?.data?.enabledLanguages, form]);
|
|
111
54
|
|
|
112
|
-
const accept = useMemo(() => {
|
|
113
|
-
const value = storageRules?.mimetype;
|
|
114
|
-
if (!value) {
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
return Array.isArray(value) ? value.join(',') : value;
|
|
118
|
-
}, [storageRules?.mimetype]);
|
|
119
|
-
|
|
120
|
-
const beforeUpload = async (file: RcFile) => {
|
|
121
|
-
const sizeLimit = storageRules?.size;
|
|
122
|
-
|
|
123
|
-
if (typeof sizeLimit === 'number' && sizeLimit > 0 && file.size > sizeLimit) {
|
|
124
|
-
message.error(t('File size exceeds the limit'));
|
|
125
|
-
return Upload.LIST_IGNORE;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const mimetypeRule = storageRules?.mimetype;
|
|
129
|
-
const normalizedRule = Array.isArray(mimetypeRule) ? mimetypeRule.join(',').trim() : `${mimetypeRule || ''}`.trim();
|
|
130
|
-
|
|
131
|
-
if (!normalizedRule || normalizedRule === '*') {
|
|
132
|
-
return file;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let targetFile = file;
|
|
136
|
-
if (!targetFile.type) {
|
|
137
|
-
const extname = targetFile.name?.match(/\.[^.]+$/)?.[0];
|
|
138
|
-
if (extname) {
|
|
139
|
-
targetFile = new File([targetFile], targetFile.name, {
|
|
140
|
-
type: mime.getType(extname) || 'application/octet-stream',
|
|
141
|
-
lastModified: targetFile.lastModified,
|
|
142
|
-
}) as RcFile;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const isAllowed = normalizedRule
|
|
147
|
-
.split(',')
|
|
148
|
-
.filter(Boolean)
|
|
149
|
-
.some((rule) => match(targetFile.type)(rule));
|
|
150
|
-
|
|
151
|
-
if (!isAllowed) {
|
|
152
|
-
message.error(t('File type is not allowed'));
|
|
153
|
-
return Upload.LIST_IGNORE;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return targetFile;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const handleLogoUpload = async (options: UploadRequestOption) => {
|
|
160
|
-
const { file, onError, onSuccess } = options;
|
|
161
|
-
setUploading(true);
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const formData = new FormData();
|
|
165
|
-
formData.append('file', file as RcFile);
|
|
166
|
-
|
|
167
|
-
const response = await app.apiClient.request({
|
|
168
|
-
url: 'attachments:create',
|
|
169
|
-
method: 'post',
|
|
170
|
-
data: formData,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const nextLogo = response?.data?.data;
|
|
174
|
-
|
|
175
|
-
form.setFieldValue('logo', nextLogo);
|
|
176
|
-
onSuccess?.(response?.data, file as RcFile);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
onError?.(error);
|
|
179
|
-
} finally {
|
|
180
|
-
setUploading(false);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
|
|
184
55
|
const handleSubmit = async () => {
|
|
185
56
|
const values = await form.validateFields();
|
|
186
57
|
const payload = {
|
|
@@ -244,44 +115,7 @@ export const SystemSettingsPage = () => {
|
|
|
244
115
|
<Input.TextArea autoSize={{ minRows: 2, maxRows: 4 }} />
|
|
245
116
|
</Form.Item>
|
|
246
117
|
<Form.Item name="logo" label={t('Logo')}>
|
|
247
|
-
<
|
|
248
|
-
{logo?.url ? (
|
|
249
|
-
<img
|
|
250
|
-
alt="system-logo"
|
|
251
|
-
src={logo.url}
|
|
252
|
-
style={{
|
|
253
|
-
width: 96,
|
|
254
|
-
height: 96,
|
|
255
|
-
objectFit: 'contain',
|
|
256
|
-
borderRadius: token.borderRadius,
|
|
257
|
-
border: `${token.lineWidth}px solid ${token.colorBorder}`,
|
|
258
|
-
background: token.colorBgLayout,
|
|
259
|
-
padding: token.paddingXS,
|
|
260
|
-
}}
|
|
261
|
-
/>
|
|
262
|
-
) : null}
|
|
263
|
-
<Space>
|
|
264
|
-
<Upload
|
|
265
|
-
showUploadList={false}
|
|
266
|
-
customRequest={handleLogoUpload}
|
|
267
|
-
beforeUpload={beforeUpload}
|
|
268
|
-
accept={accept}
|
|
269
|
-
>
|
|
270
|
-
<Button loading={uploading} icon={<UploadOutlined />}>
|
|
271
|
-
{t('Upload')}
|
|
272
|
-
</Button>
|
|
273
|
-
</Upload>
|
|
274
|
-
{logo ? (
|
|
275
|
-
<Button
|
|
276
|
-
onClick={() => {
|
|
277
|
-
form.setFieldValue('logo', null);
|
|
278
|
-
}}
|
|
279
|
-
>
|
|
280
|
-
{t('Delete')}
|
|
281
|
-
</Button>
|
|
282
|
-
) : null}
|
|
283
|
-
</Space>
|
|
284
|
-
</Space>
|
|
118
|
+
<AttachmentUpload preview={{ width: 96, height: 96, fit: 'contain' }} />
|
|
285
119
|
</Form.Item>
|
|
286
120
|
<Form.Item name="enabledLanguages" label={t('Enabled languages')} rules={[{ type: 'array', min: 1 }]}>
|
|
287
121
|
<Select mode="multiple" options={languageOptions} />
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PluginSettingsPageType } from '../PluginSettingsManager';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Settings 顶层分组。
|
|
14
|
+
*
|
|
15
|
+
* 顶栏铺一级(分组),左侧栏铺该分组下的二级 / 三级菜单。
|
|
16
|
+
* 分组归属优先取配置项自身的 `group` 字段,其次落到内置映射表,最后兜底到 `system`,
|
|
17
|
+
* 因此第三方插件不改代码也能出现在设置中心里。
|
|
18
|
+
*/
|
|
19
|
+
export type SettingsGroupKey = 'applications' | 'plugins' | 'data' | 'access' | 'automation' | 'ai' | 'system';
|
|
20
|
+
|
|
21
|
+
export type SettingsGroupDefinition = {
|
|
22
|
+
key: SettingsGroupKey;
|
|
23
|
+
/** 未翻译的分组标题,交由调用方过 i18n */
|
|
24
|
+
title: string;
|
|
25
|
+
/**
|
|
26
|
+
* 分组的门面配置项。
|
|
27
|
+
*
|
|
28
|
+
* 它会被排到该分组的第一位,并作为点击顶栏分组时的落点;
|
|
29
|
+
* 不存在(插件没装 / 无权限)时自然退回到分组内的第一项。
|
|
30
|
+
*/
|
|
31
|
+
primary?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 顶栏铺六个独立入口 + 一个「其他设置」兜底分组。
|
|
36
|
+
*
|
|
37
|
+
* 前六个各自只有一个配置项、且都是日常高频,直接当一级入口(标题取配置项自己的名字,
|
|
38
|
+
* 顺带继承插件的翻译);其余零散配置全部收进其他设置,靠左栏和悬浮下拉展开。
|
|
39
|
+
*
|
|
40
|
+
* 兜底分组不叫「系统设置」:它下面本来就有一个同名的配置项(站点标题、logo 那个),
|
|
41
|
+
* 分组和子项重名会让人以为点进去是同一个东西。
|
|
42
|
+
*/
|
|
43
|
+
export const SETTINGS_GROUPS: SettingsGroupDefinition[] = [
|
|
44
|
+
{ key: 'applications', title: 'Applications', primary: 'multi-portal' },
|
|
45
|
+
{ key: 'plugins', title: 'Plugin manager', primary: 'plugin-manager' },
|
|
46
|
+
{ key: 'data', title: 'Data sources', primary: 'data-source-manager' },
|
|
47
|
+
{ key: 'access', title: 'Users & Permissions', primary: 'users-permissions' },
|
|
48
|
+
{ key: 'automation', title: 'Workflow', primary: 'workflow' },
|
|
49
|
+
{ key: 'ai', title: 'AI employees', primary: 'ai' },
|
|
50
|
+
{ key: 'system', title: 'Other settings' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_SETTINGS_GROUP: SettingsGroupKey = 'system';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 内置配置项到分组的映射。
|
|
57
|
+
*
|
|
58
|
+
* key 为 `pluginSettingsManager.addMenuItem` 时传入的 `key`;
|
|
59
|
+
* 没列出来的一律落到其他设置,第三方插件不改代码也能出现。
|
|
60
|
+
*/
|
|
61
|
+
const BUILTIN_SETTINGS_GROUP_MAP: Record<string, SettingsGroupKey> = {
|
|
62
|
+
'multi-portal': 'applications',
|
|
63
|
+
'plugin-manager': 'plugins',
|
|
64
|
+
'data-source-manager': 'data',
|
|
65
|
+
'users-permissions': 'access',
|
|
66
|
+
workflow: 'automation',
|
|
67
|
+
ai: 'ai',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 解析某个顶级配置项所属的分组。
|
|
72
|
+
*
|
|
73
|
+
* @param {PluginSettingsPageType | null | undefined} setting 顶级配置项
|
|
74
|
+
* @returns {SettingsGroupKey} 分组 key
|
|
75
|
+
*/
|
|
76
|
+
export function getSettingsGroupKey(setting?: PluginSettingsPageType | null): SettingsGroupKey {
|
|
77
|
+
if (!setting) {
|
|
78
|
+
return DEFAULT_SETTINGS_GROUP;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const declared = (setting as PluginSettingsPageType & { group?: string }).group;
|
|
82
|
+
if (declared && SETTINGS_GROUPS.some((group) => group.key === declared)) {
|
|
83
|
+
return declared as SettingsGroupKey;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return BUILTIN_SETTINGS_GROUP_MAP[setting.name] || DEFAULT_SETTINGS_GROUP;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 按分组切分顶级配置项,并丢掉没有任何配置项的空分组。
|
|
91
|
+
*
|
|
92
|
+
* @param {PluginSettingsPageType[]} settings 已排序的顶级配置项
|
|
93
|
+
* @returns 非空分组
|
|
94
|
+
*/
|
|
95
|
+
export function groupTopLevelSettings(settings: readonly PluginSettingsPageType[] = []) {
|
|
96
|
+
return SETTINGS_GROUPS.map((group) => {
|
|
97
|
+
const members = settings.filter((setting) => getSettingsGroupKey(setting) === group.key);
|
|
98
|
+
const primaryIndex = group.primary ? members.findIndex((item) => item.name === group.primary) : -1;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...group,
|
|
102
|
+
settings:
|
|
103
|
+
primaryIndex > 0
|
|
104
|
+
? [members[primaryIndex], ...members.slice(0, primaryIndex), ...members.slice(primaryIndex + 1)]
|
|
105
|
+
: members,
|
|
106
|
+
};
|
|
107
|
+
}).filter((group) => group.settings.length > 0);
|
|
108
|
+
}
|