@nocobase/client-v2 3.0.0-alpha.1 → 3.0.0-alpha.11
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 +12 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +6 -2
- package/es/flow/actions/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +11 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +140 -140
- package/es/nocobase-buildin-plugin/index.d.ts +1 -0
- package/es/settings-app/SettingsGroupNav.d.ts +3 -2
- package/es/settings-app/SettingsSearch.d.ts +1 -1
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +145 -145
- package/package.json +7 -7
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +81 -33
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +37 -0
- package/src/__tests__/settings-center.test.tsx +312 -11
- package/src/__tests__/settings-layout-root.test.tsx +346 -6
- package/src/__tests__/settings-runtime-paths.test.ts +1 -1
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +62 -1
- package/src/acl/ACLProvider.tsx +34 -3
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +13 -5
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +12 -3
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/CollectionBlockModel.tsx +32 -1
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +18 -8
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/index.ts +2 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/nocobase-buildin-plugin/index.tsx +14 -1
- package/src/settings-app/SettingsGroupNav.tsx +11 -11
- package/src/settings-app/SettingsSearch.tsx +115 -10
- package/src/settings-app/SettingsShell.tsx +23 -6
- package/src/settings-app/runtimePaths.ts +0 -1
- package/src/settings-app/settingsTheme.ts +82 -14
- package/src/settings-center/AdminSettingsLayout.tsx +132 -107
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +1 -1
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -10,46 +10,81 @@
|
|
|
10
10
|
import { PageHeader } from '@ant-design/pro-layout';
|
|
11
11
|
import { css } from '@emotion/css';
|
|
12
12
|
import { FlowModelRenderer, useFlowEngine } from '@nocobase/flow-engine';
|
|
13
|
-
import { Layout,
|
|
13
|
+
import { Layout, Result, Tabs, theme } from 'antd';
|
|
14
14
|
import React, { useEffect, useMemo, useRef } from 'react';
|
|
15
15
|
import { useTranslation } from 'react-i18next';
|
|
16
|
-
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
16
|
+
import { generatePath, Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
17
|
+
import { getModernClientPrefix } from '../authRedirect';
|
|
17
18
|
import type { PluginSettingsPageType } from '../PluginSettingsManager';
|
|
18
19
|
import { useApp } from '../hooks/useApp';
|
|
20
|
+
import { resolveSettingsAppScopeWithinPublicPath } from '../settings-app/settingsDocumentPath';
|
|
19
21
|
import { AdminSettingsLayoutModel } from './AdminSettingsLayoutModel';
|
|
20
22
|
import { useSettingsGroups } from './useSettingsGroups';
|
|
21
23
|
import {
|
|
22
24
|
ADMIN_SETTINGS_LAYOUT_MODEL_UID,
|
|
23
25
|
createSettingsPathMap,
|
|
24
|
-
findSettingsByName,
|
|
25
26
|
getDefaultSettingsPath,
|
|
26
|
-
getSidebarMenuItems,
|
|
27
|
-
getSidebarSelectedKey,
|
|
28
27
|
matchSettingsRoute,
|
|
29
28
|
} from './utils';
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Maximum width of the content area.
|
|
32
|
+
*
|
|
33
|
+
* Centers and narrows the content on wide screens: settings pages are mostly forms
|
|
34
|
+
* and lists, and across 2560px a single line sweeps the whole display, which is
|
|
35
|
+
* tiring to read and leaves the actions on the right far from the title on the left.
|
|
36
|
+
*/
|
|
37
|
+
const SETTINGS_CONTENT_MAX_WIDTH = 1280;
|
|
38
|
+
|
|
39
|
+
function AdminDocumentRedirect() {
|
|
40
|
+
const app = useApp();
|
|
41
|
+
const rootPublicPath = app.getPublicPath().replace(/\/+$/, '');
|
|
42
|
+
const appScope = resolveSettingsAppScopeWithinPublicPath(app.getPublicPath(), app.router.getBasename?.());
|
|
43
|
+
const targetPath = `${rootPublicPath}/${getModernClientPrefix()}${appScope}/admin`;
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
window.location.replace(targetPath);
|
|
47
|
+
}, [targetPath]);
|
|
48
|
+
|
|
49
|
+
return app.renderComponent('AppSpin');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function SettingsEmpty(props: { type: 'forbidden' | 'home' | 'not-found' }) {
|
|
32
53
|
const { type } = props;
|
|
33
54
|
const { t } = useTranslation();
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
let status: '403' | '404' | 'info';
|
|
57
|
+
let title: string;
|
|
58
|
+
let subTitle: string;
|
|
59
|
+
|
|
60
|
+
if (type === 'forbidden') {
|
|
61
|
+
status = '403';
|
|
62
|
+
title = t('Your current role cannot access Settings');
|
|
63
|
+
subTitle = t('Switch to a role with access, or contact an administrator to request access.');
|
|
64
|
+
} else if (type === 'not-found') {
|
|
65
|
+
status = '404';
|
|
66
|
+
title = t('Settings page not found');
|
|
67
|
+
subTitle = t('The settings page you requested does not exist or has been removed.');
|
|
68
|
+
} else {
|
|
69
|
+
status = 'info';
|
|
70
|
+
title = t('No settings pages available');
|
|
71
|
+
subTitle = t(
|
|
72
|
+
'No settings pages are currently available in Client V2. Settings registered by migrated plugins will appear here automatically.',
|
|
42
73
|
);
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
return (
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
<div role="status" aria-atomic="true">
|
|
78
|
+
<Result
|
|
79
|
+
status={status}
|
|
80
|
+
title={
|
|
81
|
+
<span role="heading" aria-level={1}>
|
|
82
|
+
{title}
|
|
83
|
+
</span>
|
|
84
|
+
}
|
|
85
|
+
subTitle={subTitle}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
53
88
|
);
|
|
54
89
|
}
|
|
55
90
|
|
|
@@ -63,9 +98,12 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
63
98
|
const app = useApp();
|
|
64
99
|
const navigate = useNavigate();
|
|
65
100
|
const location = useLocation();
|
|
101
|
+
const params = useParams();
|
|
102
|
+
const routeParams = Object.fromEntries(
|
|
103
|
+
Object.entries(params).filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
|
|
104
|
+
);
|
|
66
105
|
const { token } = theme.useToken();
|
|
67
106
|
const {
|
|
68
|
-
activeGroupSettings,
|
|
69
107
|
allSettings,
|
|
70
108
|
currentSetting,
|
|
71
109
|
currentTopLevelSetting,
|
|
@@ -87,10 +125,9 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
87
125
|
const settingsRootPath = app.pluginSettingsManager.getRoutePath('');
|
|
88
126
|
const settingsRootPathWithoutTrailingSlash = settingsRootPath.replace(/\/$/, '');
|
|
89
127
|
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
const shouldShowSidebar = activeGroupSettings.length > 1;
|
|
128
|
+
// No sidebar: single-entry groups never needed one, and multi-entry groups such as
|
|
129
|
+
// "other settings" now use the hover dropdown in the top bar. Children of a
|
|
130
|
+
// top-level setting (users / roles / sync) keep using the tabs under the header.
|
|
94
131
|
// 子页面一律走页头下的 Tab:左栏只表达「哪个模块」,模块内部的分页交给 Tab,
|
|
95
132
|
// 和 v1 设置中心保持一致。
|
|
96
133
|
const pageTabs = useMemo(() => {
|
|
@@ -108,13 +145,6 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
108
145
|
);
|
|
109
146
|
return matched?.key ?? pageTabs[0]?.key;
|
|
110
147
|
}, [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
148
|
// 页头只需要补一句「当前在哪个子页」。
|
|
119
149
|
const pageSubTitle =
|
|
120
150
|
currentVisibleSetting && currentVisibleSetting.title !== currentTopLevelSetting?.title
|
|
@@ -136,6 +166,18 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
136
166
|
location.pathname === settingsRootPath ||
|
|
137
167
|
location.pathname === settingsRootPathWithoutTrailingSlash ||
|
|
138
168
|
location.pathname === `${settingsRootPath}index`;
|
|
169
|
+
const currentPathWithoutTrailingSlash = location.pathname.replace(/\/+$/, '');
|
|
170
|
+
const currentTopLevelPathWithoutTrailingSlash = currentTopLevelSetting?.path.replace(/\/+$/, '');
|
|
171
|
+
const isTopLevelSettingsPath =
|
|
172
|
+
currentPathWithoutTrailingSlash === currentTopLevelPathWithoutTrailingSlash &&
|
|
173
|
+
!!currentTopLevelSetting?.children?.length;
|
|
174
|
+
const visibleIndexPath = isTopLevelSettingsPath
|
|
175
|
+
? currentVisibleTopLevelSetting?.children?.find((item) => item.pageKey === 'index')?.path
|
|
176
|
+
: undefined;
|
|
177
|
+
const firstVisibleChildPath = isTopLevelSettingsPath
|
|
178
|
+
? getDefaultSettingsPath(currentVisibleTopLevelSetting?.children as PluginSettingsPageType[])
|
|
179
|
+
: undefined;
|
|
180
|
+
const nextVisibleChildPath = visibleIndexPath || firstVisibleChildPath;
|
|
139
181
|
|
|
140
182
|
if (shouldRedirectToDefault && defaultSettingsPath) {
|
|
141
183
|
return <Navigate replace to={defaultSettingsPath} />;
|
|
@@ -146,27 +188,39 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
146
188
|
}
|
|
147
189
|
|
|
148
190
|
if (!currentSetting) {
|
|
149
|
-
return <SettingsEmpty type="
|
|
191
|
+
return <SettingsEmpty type="not-found" />;
|
|
150
192
|
}
|
|
151
193
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
194
|
+
if (currentSetting.isAllow === false) {
|
|
195
|
+
const firstVisibleTabPath = (currentVisibleTopLevelSetting?.children || [])
|
|
196
|
+
.map((tab) => getDefaultSettingsPath([tab]))
|
|
197
|
+
.filter((path): path is string => typeof path === 'string')
|
|
198
|
+
.map((path) => {
|
|
199
|
+
try {
|
|
200
|
+
return generatePath(path, routeParams);
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
.find((path): path is string => typeof path === 'string');
|
|
206
|
+
|
|
207
|
+
if (firstVisibleTabPath) {
|
|
208
|
+
return <Navigate replace to={firstVisibleTabPath} />;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!defaultSettingsPath) {
|
|
212
|
+
return <AdminDocumentRedirect />;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return <SettingsEmpty type="forbidden" />;
|
|
154
216
|
}
|
|
155
217
|
|
|
156
218
|
if (currentSetting.link) {
|
|
157
219
|
return <Navigate replace to={currentSetting.link} />;
|
|
158
220
|
}
|
|
159
221
|
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
const firstVisibleChildPath = getDefaultSettingsPath(
|
|
163
|
-
currentVisibleTopLevelSetting?.children as PluginSettingsPageType[],
|
|
164
|
-
);
|
|
165
|
-
const nextPath = visibleIndexPath || firstVisibleChildPath;
|
|
166
|
-
|
|
167
|
-
if (nextPath && nextPath !== location.pathname) {
|
|
168
|
-
return <Navigate replace to={nextPath} />;
|
|
169
|
-
}
|
|
222
|
+
if (nextVisibleChildPath && nextVisibleChildPath !== location.pathname) {
|
|
223
|
+
return <Navigate replace to={nextVisibleChildPath} />;
|
|
170
224
|
}
|
|
171
225
|
|
|
172
226
|
return (
|
|
@@ -179,43 +233,6 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
179
233
|
overflow: 'hidden',
|
|
180
234
|
}}
|
|
181
235
|
>
|
|
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',
|
|
191
|
-
}}
|
|
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}
|
|
219
236
|
<Layout.Content
|
|
220
237
|
style={{
|
|
221
238
|
background: token.colorBgLayout,
|
|
@@ -239,31 +256,39 @@ export const InternalAdminSettingsLayout = () => {
|
|
|
239
256
|
overflow: 'auto',
|
|
240
257
|
}}
|
|
241
258
|
>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
{/*
|
|
260
|
+
The header is no longer a full-width white band: title and content share one
|
|
261
|
+
background and one max width, so the page reads as a single surface rather
|
|
262
|
+
than a title bar stacked on a content area.
|
|
263
|
+
*/}
|
|
264
|
+
<div style={{ margin: '0 auto', maxWidth: SETTINGS_CONTENT_MAX_WIDTH, padding: token.paddingLG }}>
|
|
265
|
+
<PageHeader
|
|
266
|
+
ghost
|
|
267
|
+
title={currentTopLevelSetting?.title}
|
|
268
|
+
subTitle={pageTabs.length ? undefined : pageSubTitle}
|
|
269
|
+
footer={
|
|
270
|
+
pageTabs.length ? (
|
|
271
|
+
<Tabs
|
|
272
|
+
activeKey={activeTabKey}
|
|
273
|
+
items={pageTabs}
|
|
274
|
+
tabBarStyle={{ marginBottom: 0 }}
|
|
275
|
+
onChange={(key) => {
|
|
276
|
+
if (key !== location.pathname) {
|
|
277
|
+
navigate(key);
|
|
278
|
+
}
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
) : undefined
|
|
282
|
+
}
|
|
283
|
+
// With the white band gone there is nothing separating header from content,
|
|
284
|
+
// so spacing does the job the bottom border used to do implicitly.
|
|
285
|
+
style={{
|
|
286
|
+
background: 'transparent',
|
|
287
|
+
marginBottom: token.marginLG,
|
|
288
|
+
paddingBlock: 0,
|
|
289
|
+
paddingInline: 0,
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
267
292
|
<Outlet />
|
|
268
293
|
</div>
|
|
269
294
|
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
11
|
+
import { SETTINGS_GROUPS } from '../groups';
|
|
12
|
+
|
|
13
|
+
describe('settings center groups', () => {
|
|
14
|
+
it('places the plugin manager immediately after AI employees', () => {
|
|
15
|
+
expect(SETTINGS_GROUPS.map((group) => group.key)).toEqual([
|
|
16
|
+
'applications',
|
|
17
|
+
'data',
|
|
18
|
+
'access',
|
|
19
|
+
'automation',
|
|
20
|
+
'ai',
|
|
21
|
+
'plugins',
|
|
22
|
+
'system',
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -42,11 +42,11 @@ export type SettingsGroupDefinition = {
|
|
|
42
42
|
*/
|
|
43
43
|
export const SETTINGS_GROUPS: SettingsGroupDefinition[] = [
|
|
44
44
|
{ key: 'applications', title: 'Applications', primary: 'multi-portal' },
|
|
45
|
-
{ key: 'plugins', title: 'Plugin manager', primary: 'plugin-manager' },
|
|
46
45
|
{ key: 'data', title: 'Data sources', primary: 'data-source-manager' },
|
|
47
46
|
{ key: 'access', title: 'Users & Permissions', primary: 'users-permissions' },
|
|
48
47
|
{ key: 'automation', title: 'Workflow', primary: 'workflow' },
|
|
49
48
|
{ key: 'ai', title: 'AI employees', primary: 'ai' },
|
|
49
|
+
{ key: 'plugins', title: 'Plugin manager', primary: 'plugin-manager' },
|
|
50
50
|
{ key: 'system', title: 'Other settings' },
|
|
51
51
|
];
|
|
52
52
|
|
|
@@ -0,0 +1,33 @@
|
|
|
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 {
|
|
11
|
+
decodeUIOperation as decode,
|
|
12
|
+
encodeUIOperation as encode,
|
|
13
|
+
parseUIOperation as parse,
|
|
14
|
+
removeUIOperation,
|
|
15
|
+
UI_OPERATION_QUERY_KEY,
|
|
16
|
+
type UIOperation,
|
|
17
|
+
} from '@nocobase/shared';
|
|
18
|
+
import { browserUIOperationCodec } from './ui-operation-codec';
|
|
19
|
+
|
|
20
|
+
export function parseUIOperation(search: string): UIOperation | undefined {
|
|
21
|
+
return parse(search, browserUIOperationCodec);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function decodeUIOperation(encoded: string): UIOperation | undefined {
|
|
25
|
+
return decode(encoded, browserUIOperationCodec);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function encodeUIOperation(operation: UIOperation): string {
|
|
29
|
+
return encode(operation, browserUIOperationCodec);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { browserUIOperationCodec, removeUIOperation, UI_OPERATION_QUERY_KEY };
|
|
33
|
+
export type { UIOperation };
|
|
@@ -0,0 +1,54 @@
|
|
|
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 { UIOperationCodec } from '@nocobase/shared';
|
|
11
|
+
|
|
12
|
+
const base64UrlPattern = /^[A-Za-z0-9_-]*$/;
|
|
13
|
+
const utf8Encoder = new TextEncoder();
|
|
14
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
15
|
+
|
|
16
|
+
function bytesToBinaryString(bytes: Uint8Array): string {
|
|
17
|
+
let result = '';
|
|
18
|
+
for (const byte of bytes) {
|
|
19
|
+
result += String.fromCharCode(byte);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function binaryStringToBytes(value: string): Uint8Array {
|
|
25
|
+
const bytes = new Uint8Array(value.length);
|
|
26
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
27
|
+
bytes[index] = value.charCodeAt(index);
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function encodeBrowserUIOperation(value: string): string {
|
|
33
|
+
return btoa(bytesToBinaryString(utf8Encoder.encode(value)))
|
|
34
|
+
.replace(/\+/g, '-')
|
|
35
|
+
.replace(/\//g, '_')
|
|
36
|
+
.replace(/=+$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const browserUIOperationCodec: UIOperationCodec = {
|
|
40
|
+
encode: encodeBrowserUIOperation,
|
|
41
|
+
decode(value) {
|
|
42
|
+
if (!base64UrlPattern.test(value) || value.length % 4 === 1) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const paddedValue = `${value.replace(/-/g, '+').replace(/_/g, '/')}${'='.repeat((4 - (value.length % 4)) % 4)}`;
|
|
48
|
+
const decoded = utf8Decoder.decode(binaryStringToBytes(atob(paddedValue)));
|
|
49
|
+
return encodeBrowserUIOperation(decoded) === value ? decoded : undefined;
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|