@nocobase/client-v2 2.3.0-alpha.1 → 3.0.0-alpha.2
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/acl/ACLProvider.d.ts +10 -0
- package/es/authRedirect.d.ts +5 -0
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/linkageRules.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- 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/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/index.d.ts +4 -1
- package/es/index.mjs +139 -125
- 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/lib/index.js +141 -127
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +8 -7
- package/src/__tests__/authRedirect.test.ts +137 -0
- package/src/__tests__/browserChecker.test.ts +124 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +51 -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/components/AttachmentUpload.tsx +275 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
- package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- 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/linkageRules.tsx +26 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
- 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__/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/FlowRoute.tsx +71 -29
- package/src/flow/index.ts +2 -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/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- 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 +27 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
- 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/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/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +10 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +99 -71
- 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
|
@@ -0,0 +1,100 @@
|
|
|
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 { theme as antdTheme, type ThemeConfig } from 'antd';
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import { useApp } from '../hooks/useApp';
|
|
13
|
+
|
|
14
|
+
/** 简约主题在 `themeConfig` 表里的固定标识,见 plugin-theme-editor 的 builtinThemes */
|
|
15
|
+
export const MINIMAL_THEME_UID = 'minimal';
|
|
16
|
+
|
|
17
|
+
const ALGORITHMS = {
|
|
18
|
+
defaultAlgorithm: antdTheme.defaultAlgorithm,
|
|
19
|
+
darkAlgorithm: antdTheme.darkAlgorithm,
|
|
20
|
+
compactAlgorithm: antdTheme.compactAlgorithm,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 把库里存的 algorithm 名字换成 antd 的算法函数。
|
|
25
|
+
*
|
|
26
|
+
* 主题记录里存的是字符串(JSON 存不下函数),可能是单个也可能是数组。
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown} value 库里存的 algorithm 字段
|
|
29
|
+
* @returns {ThemeConfig['algorithm'] | undefined} antd 算法
|
|
30
|
+
*/
|
|
31
|
+
function toAlgorithm(value: unknown): ThemeConfig['algorithm'] | undefined {
|
|
32
|
+
const names = (Array.isArray(value) ? value : [value]).filter((name): name is keyof typeof ALGORITHMS =>
|
|
33
|
+
Boolean(name && typeof name === 'string' && ALGORITHMS[name]),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!names.length) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return names.length === 1 ? ALGORITHMS[names[0]] : names.map((name) => ALGORITHMS[name]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 读取设置中心主题。
|
|
45
|
+
*
|
|
46
|
+
* 设置中心的外观由 `themeConfig` 里那条「简约」记录约束,在主题编辑器里就能改;
|
|
47
|
+
* 它本身是一条普通的可选主题,别的应用要同一套观感直接选它即可。
|
|
48
|
+
* 读不到(主题编辑器插件没启、老实例还没迁移、请求失败)时返回 `null`,
|
|
49
|
+
* 由调用方回落到代码里的默认中性配色,避免设置中心突然变回一片蓝。
|
|
50
|
+
*
|
|
51
|
+
* @returns {ThemeConfig | null | undefined} 主题配置;`undefined` 表示还没读完
|
|
52
|
+
*/
|
|
53
|
+
export function useSettingsThemeConfig(): ThemeConfig | null | undefined {
|
|
54
|
+
const app = useApp();
|
|
55
|
+
const [themeConfig, setThemeConfig] = useState<ThemeConfig | null | undefined>(undefined);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
|
|
60
|
+
const load = async () => {
|
|
61
|
+
try {
|
|
62
|
+
const response = await app.apiClient.request({
|
|
63
|
+
resource: 'themeConfig',
|
|
64
|
+
action: 'list',
|
|
65
|
+
params: { paginate: false },
|
|
66
|
+
});
|
|
67
|
+
const items: any[] = response?.data?.data || [];
|
|
68
|
+
const matched = items.find((item) => item?.uid === MINIMAL_THEME_UID);
|
|
69
|
+
const config = matched?.config;
|
|
70
|
+
|
|
71
|
+
if (cancelled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!config) {
|
|
76
|
+
setThemeConfig(null);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setThemeConfig({
|
|
81
|
+
token: config.token,
|
|
82
|
+
components: config.components,
|
|
83
|
+
algorithm: toAlgorithm(config.algorithm),
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!cancelled) {
|
|
87
|
+
setThemeConfig(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
void load();
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
};
|
|
97
|
+
}, [app.apiClient]);
|
|
98
|
+
|
|
99
|
+
return themeConfig;
|
|
100
|
+
}
|
|
@@ -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} />
|