@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
|
@@ -14,12 +14,11 @@ import { Button, Dropdown, theme, Tooltip, type ButtonProps, type MenuProps } fr
|
|
|
14
14
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
15
15
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import {
|
|
17
|
+
import { useLocation } from 'react-router-dom';
|
|
18
18
|
import { useACLRoleContext } from '../../../acl';
|
|
19
|
-
import type { BaseApplication } from '../../../BaseApplication';
|
|
20
19
|
import type { PluginSettingsPageType } from '../../../PluginSettingsManager';
|
|
21
|
-
import { shouldOpenAdminRouteInNewWindow } from '../../../RouterManager';
|
|
22
20
|
import { useApp } from '../../../hooks/useApp';
|
|
21
|
+
import { resolveStandaloneSettingsPath } from '../../../settings-app/runtimePaths';
|
|
23
22
|
import {
|
|
24
23
|
filterRenderableSettings,
|
|
25
24
|
filterVisibleSettings,
|
|
@@ -66,109 +65,6 @@ const topbarActionTriggerClassName = css`
|
|
|
66
65
|
height: 100%;
|
|
67
66
|
`;
|
|
68
67
|
|
|
69
|
-
type TopbarSettingsAppLike = Pick<
|
|
70
|
-
BaseApplication<any>,
|
|
71
|
-
'name' | 'router' | 'getPublicPath' | 'getHref' | 'layoutManager'
|
|
72
|
-
>;
|
|
73
|
-
|
|
74
|
-
const normalizeTopbarPath = (pathname?: string) => {
|
|
75
|
-
const trimmed = pathname?.trim();
|
|
76
|
-
if (!trimmed || trimmed === '/') {
|
|
77
|
-
return '/';
|
|
78
|
-
}
|
|
79
|
-
return `/${trimmed.replace(/^\/+/, '')}`;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const normalizeTopbarBasePath = (pathname?: string) => {
|
|
83
|
-
const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
|
|
84
|
-
return normalized === '' || normalized === '/' ? '' : normalized;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
|
|
88
|
-
return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
|
|
92
|
-
const normalizedPath = normalizeTopbarPath(pathname);
|
|
93
|
-
const normalizedBase = normalizeTopbarBasePath(basename);
|
|
94
|
-
|
|
95
|
-
if (!normalizedBase) {
|
|
96
|
-
return normalizedPath;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (normalizedPath === normalizedBase) {
|
|
100
|
-
return '/';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (normalizedPath.startsWith(`${normalizedBase}/`)) {
|
|
104
|
-
return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return normalizedPath;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const getTopbarAppPath = (pathname: string) => {
|
|
111
|
-
const normalizedPath = normalizeTopbarPath(pathname);
|
|
112
|
-
const match = /^(.*?\/(?:apps|_app)\/[^/]+)(?=\/|$)/.exec(normalizedPath);
|
|
113
|
-
const appPath = match?.[1] || '';
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
appPath,
|
|
117
|
-
routePath: appPath ? normalizeTopbarPath(normalizedPath.slice(appPath.length)) : normalizedPath,
|
|
118
|
-
};
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const getTopbarContextAppPath = (app: TopbarSettingsAppLike | undefined, basename?: string) => {
|
|
122
|
-
const basenameAppPath = getTopbarAppPath(basename || '').appPath;
|
|
123
|
-
if (basenameAppPath) {
|
|
124
|
-
return basenameAppPath;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const publicPathAppPath = getTopbarAppPath(app?.getPublicPath?.() || '').appPath;
|
|
128
|
-
if (publicPathAppPath) {
|
|
129
|
-
return publicPathAppPath;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (app?.name && app.name !== 'main' && app.getHref) {
|
|
133
|
-
return normalizeTopbarBasePath(app.getHref('/'));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return '';
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const prependTopbarAppPath = (pathname: string, appPath: string) => {
|
|
140
|
-
const normalizedPath = normalizeTopbarPath(pathname);
|
|
141
|
-
|
|
142
|
-
if (!appPath || normalizedPath === appPath || normalizedPath.startsWith(`${appPath}/`)) {
|
|
143
|
-
return normalizedPath;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return `${appPath}${normalizedPath}`;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const isAdminRuntimePath = (pathname: string) => {
|
|
150
|
-
return pathname === '/admin' || pathname.startsWith('/admin/');
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const getTopbarAdminRoutePath = (app: TopbarSettingsAppLike | undefined) => {
|
|
154
|
-
try {
|
|
155
|
-
return app?.layoutManager?.getLayout?.('admin')?.routePath;
|
|
156
|
-
} catch {
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
|
|
162
|
-
const normalizedTarget = normalizeTopbarPath(targetPath);
|
|
163
|
-
const normalizedBase = normalizeTopbarBasePath(basename);
|
|
164
|
-
|
|
165
|
-
if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
|
|
166
|
-
return normalizedTarget;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return `${normalizedBase}${normalizedTarget}`;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
68
|
function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
|
|
173
69
|
return (
|
|
174
70
|
<div
|
|
@@ -185,39 +81,13 @@ function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: str
|
|
|
185
81
|
const app = useApp();
|
|
186
82
|
const location = useLocation();
|
|
187
83
|
const targetPath = props.path || '/admin/settings';
|
|
188
|
-
const basename = getTopbarRouterBasePath(app);
|
|
189
|
-
const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
|
|
190
|
-
const currentLocationAppPath = getTopbarAppPath(currentPath);
|
|
191
|
-
const currentAppPath = currentLocationAppPath.appPath || getTopbarContextAppPath(app, basename);
|
|
192
|
-
const currentRoutePath = currentLocationAppPath.appPath ? currentLocationAppPath.routePath : currentPath;
|
|
193
|
-
const targetPathInCurrentApp = prependTopbarAppPath(stripTopbarRouterBasePath(targetPath, basename), currentAppPath);
|
|
194
|
-
|
|
195
|
-
if (currentAppPath) {
|
|
196
|
-
const href = buildTopbarDocumentHref(targetPathInCurrentApp, basename);
|
|
197
|
-
const shouldOpenInNewWindow = shouldOpenAdminRouteInNewWindow({
|
|
198
|
-
currentPathname: currentPath,
|
|
199
|
-
targetPathname: targetPathInCurrentApp,
|
|
200
|
-
basePath: basename,
|
|
201
|
-
adminRoutePath: getTopbarAdminRoutePath(app),
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<a
|
|
206
|
-
href={href}
|
|
207
|
-
target={shouldOpenInNewWindow ? '_blank' : undefined}
|
|
208
|
-
rel={shouldOpenInNewWindow ? 'noopener noreferrer' : undefined}
|
|
209
|
-
>
|
|
210
|
-
{props.title}
|
|
211
|
-
</a>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (isAdminRuntimePath(currentRoutePath)) {
|
|
216
|
-
return <Link to={targetPathInCurrentApp}>{props.title}</Link>;
|
|
217
|
-
}
|
|
218
84
|
|
|
219
85
|
return (
|
|
220
|
-
<a
|
|
86
|
+
<a
|
|
87
|
+
href={resolveStandaloneSettingsPath(app, targetPath, location.pathname)}
|
|
88
|
+
target="_blank"
|
|
89
|
+
rel="noopener noreferrer"
|
|
90
|
+
>
|
|
221
91
|
{props.title}
|
|
222
92
|
</a>
|
|
223
93
|
);
|
|
@@ -238,66 +108,68 @@ export function getTopbarPluginSettingsItems(options: {
|
|
|
238
108
|
}): NonNullable<MenuProps['items']> {
|
|
239
109
|
const { settings, canManagePlugins, t } = options;
|
|
240
110
|
const topLevelSettings = filterVisibleSettings(filterRenderableSettings(settings));
|
|
241
|
-
const
|
|
111
|
+
const primarySettings = sortTopLevelSettings(
|
|
112
|
+
topLevelSettings
|
|
113
|
+
.filter(
|
|
114
|
+
(item) =>
|
|
115
|
+
((item.sort || 0) < 0 || item.name === PLUGIN_MANAGER_SETTING_NAME) &&
|
|
116
|
+
(item.name !== PLUGIN_MANAGER_SETTING_NAME || canManagePlugins),
|
|
117
|
+
)
|
|
118
|
+
.map((item) => ({
|
|
119
|
+
...item,
|
|
120
|
+
children: undefined,
|
|
121
|
+
})),
|
|
122
|
+
);
|
|
242
123
|
const settingsByKey = new Map<string, PluginSettingsPageType>();
|
|
243
124
|
const normalSettings = sortTopLevelSettings(
|
|
244
125
|
topLevelSettings
|
|
245
|
-
.filter((item) => item.name !== PLUGIN_MANAGER_SETTING_NAME)
|
|
126
|
+
.filter((item) => (item.sort || 0) >= 0 && item.name !== PLUGIN_MANAGER_SETTING_NAME)
|
|
246
127
|
.map((item) => ({
|
|
247
128
|
...item,
|
|
248
129
|
children: undefined,
|
|
249
130
|
})),
|
|
250
131
|
);
|
|
251
132
|
|
|
252
|
-
normalSettings.forEach((item) => {
|
|
133
|
+
[...primarySettings, ...normalSettings].forEach((item) => {
|
|
253
134
|
settingsByKey.set(item.key, item);
|
|
254
135
|
});
|
|
255
136
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const items: NonNullable<MenuProps['items']> = [];
|
|
282
|
-
|
|
283
|
-
if (canManagePlugins && pluginManagerSetting) {
|
|
284
|
-
items.push({
|
|
285
|
-
key: pluginManagerSetting.key,
|
|
286
|
-
icon: pluginManagerSetting.icon || <ApiOutlined />,
|
|
287
|
-
label: (
|
|
288
|
-
<TopbarInternalSettingsLabel
|
|
289
|
-
title={pluginManagerSetting.title || t('Plugin manager')}
|
|
290
|
-
path={pluginManagerSetting.path}
|
|
291
|
-
/>
|
|
292
|
-
),
|
|
137
|
+
const buildMenuItems = (targetSettings: PluginSettingsPageType[]) =>
|
|
138
|
+
(getMenuItems(targetSettings) || []).map((item: any) => {
|
|
139
|
+
if (item?.type === 'divider') {
|
|
140
|
+
return item;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const matchedSetting = settingsByKey.get(String(item.key));
|
|
144
|
+
const targetPath = matchedSetting?.path;
|
|
145
|
+
const targetLink = matchedSetting?.link;
|
|
146
|
+
const isPluginManager = matchedSetting?.name === PLUGIN_MANAGER_SETTING_NAME;
|
|
147
|
+
const targetTitle = matchedSetting?.title || (isPluginManager ? t('Plugin manager') : item.title);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
key: item.key,
|
|
151
|
+
name: matchedSetting?.name,
|
|
152
|
+
path: matchedSetting?.path,
|
|
153
|
+
link: targetLink,
|
|
154
|
+
title: targetTitle,
|
|
155
|
+
icon: isPluginManager ? matchedSetting?.icon || <ApiOutlined /> : item.icon,
|
|
156
|
+
label: targetLink ? (
|
|
157
|
+
<TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
|
|
158
|
+
) : (
|
|
159
|
+
<TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
|
|
160
|
+
),
|
|
161
|
+
};
|
|
293
162
|
});
|
|
294
|
-
}
|
|
295
163
|
|
|
296
|
-
|
|
164
|
+
const primaryItems = buildMenuItems(primarySettings);
|
|
165
|
+
const normalItems = buildMenuItems(normalSettings);
|
|
166
|
+
const items: NonNullable<MenuProps['items']> = [...primaryItems];
|
|
167
|
+
|
|
168
|
+
if (primaryItems.length && normalItems.length) {
|
|
297
169
|
items.push({ type: 'divider' });
|
|
298
170
|
}
|
|
299
171
|
|
|
300
|
-
items.push(...
|
|
172
|
+
items.push(...normalItems);
|
|
301
173
|
|
|
302
174
|
return items;
|
|
303
175
|
}
|
|
@@ -105,9 +105,6 @@ function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): DataSco
|
|
|
105
105
|
ctx,
|
|
106
106
|
parseFieldIndexEntries((ctx.model as any)?.context?.fieldIndex ?? (ctx as any)?.fieldIndex),
|
|
107
107
|
);
|
|
108
|
-
if (!entries.length) {
|
|
109
|
-
return wildcardDeps();
|
|
110
|
-
}
|
|
111
108
|
|
|
112
109
|
let parentDepth = 0;
|
|
113
110
|
let cursor = [...depPath];
|
|
@@ -121,6 +118,10 @@ function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): DataSco
|
|
|
121
118
|
return wildcardDeps();
|
|
122
119
|
}
|
|
123
120
|
|
|
121
|
+
if (!entries.length) {
|
|
122
|
+
return parentDepth === 0 ? resolveRootItemDependencyPath(cursor) : emptyDeps();
|
|
123
|
+
}
|
|
124
|
+
|
|
124
125
|
if (parentDepth === entries.length) {
|
|
125
126
|
return resolveRootItemDependencyPath(cursor);
|
|
126
127
|
}
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,14 @@ export * from './PluginSettingsManager';
|
|
|
29
29
|
export * from './layout-manager';
|
|
30
30
|
export * from './hooks';
|
|
31
31
|
export { default as languageCodes } from './locale/languageCodes';
|
|
32
|
-
export
|
|
32
|
+
export {
|
|
33
|
+
CurrentUserContext,
|
|
34
|
+
NocoBaseBuildInPlugin,
|
|
35
|
+
NocoBaseBuildInPluginV2,
|
|
36
|
+
useCurrentRoles,
|
|
37
|
+
useCurrentUserContext,
|
|
38
|
+
} from './nocobase-buildin-plugin';
|
|
39
|
+
export type { CurrentRoleOption, CurrentUserState } from './nocobase-buildin-plugin';
|
|
33
40
|
export { getRouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
|
|
34
41
|
export type { RouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
|
|
35
42
|
export * from './collection-field-interface/CollectionFieldInterface';
|
|
@@ -54,3 +61,5 @@ export {
|
|
|
54
61
|
export type { NocoBaseDesktopRoute } from './flow-compat';
|
|
55
62
|
export * from './utils/markdownSanitize';
|
|
56
63
|
export { default as AntdAppProvider } from './theme/AntdAppProvider';
|
|
64
|
+
export { isSettingsApp } from './settings-app/isSettingsApp';
|
|
65
|
+
export { MINIMAL_THEME_UID, useSettingsThemeConfig } from './settings-app/useSettingsThemeConfig';
|
|
@@ -54,6 +54,7 @@ export const languageCodes: Record<string, LocaleOptions> = {
|
|
|
54
54
|
'ml-IN': { label: 'മലയാളം' },
|
|
55
55
|
'mn-MN': { label: 'Монгол хэл' },
|
|
56
56
|
'ms-MY': { label: 'بهاس ملايو' },
|
|
57
|
+
'my-MM': { label: 'မြန်မာဘာသာ' },
|
|
57
58
|
'nb-NO': { label: 'Norsk bokmål' },
|
|
58
59
|
'ne-NP': { label: 'नेपाली' },
|
|
59
60
|
'nl-BE': { label: 'Vlaams' },
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { useSyncExternalStore } from 'react';
|
|
11
|
+
|
|
12
|
+
export type CurrentUserAuthStatus = 'unknown' | 'authenticated' | 'unauthenticated' | 'redirecting';
|
|
13
|
+
|
|
14
|
+
type AuthStatusStore = {
|
|
15
|
+
getSnapshot: () => CurrentUserAuthStatus;
|
|
16
|
+
set: (status: CurrentUserAuthStatus) => void;
|
|
17
|
+
subscribe: (listener: () => void) => () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const stores = new WeakMap<object, AuthStatusStore>();
|
|
21
|
+
|
|
22
|
+
function createAuthStatusStore(): AuthStatusStore {
|
|
23
|
+
let status: CurrentUserAuthStatus = 'unknown';
|
|
24
|
+
const listeners = new Set<() => void>();
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
getSnapshot: () => status,
|
|
28
|
+
set: (nextStatus) => {
|
|
29
|
+
if (status === nextStatus) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
status = nextStatus;
|
|
33
|
+
listeners.forEach((listener) => listener());
|
|
34
|
+
},
|
|
35
|
+
subscribe: (listener) => {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
return () => listeners.delete(listener);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getAuthStatusStore(app: object) {
|
|
43
|
+
let store = stores.get(app);
|
|
44
|
+
if (!store) {
|
|
45
|
+
store = createAuthStatusStore();
|
|
46
|
+
stores.set(app, store);
|
|
47
|
+
}
|
|
48
|
+
return store;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function setCurrentUserAuthStatus(app: object, status: CurrentUserAuthStatus) {
|
|
52
|
+
getAuthStatusStore(app).set(status);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useCurrentUserAuthStatus(app: object) {
|
|
56
|
+
const store = getAuthStatusStore(app);
|
|
57
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
58
|
+
}
|
|
@@ -9,21 +9,23 @@
|
|
|
9
9
|
|
|
10
10
|
import { createCollectionContextMeta, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
11
|
import React, { createContext, type FC, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
-
import {
|
|
12
|
+
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
13
13
|
import { useACLRoleContext } from '../acl';
|
|
14
14
|
import type { Application } from '../Application';
|
|
15
|
-
import { getCurrentV2RedirectPath,
|
|
15
|
+
import { getCurrentV2RedirectPath, redirectToV2Signin } from '../authRedirect';
|
|
16
16
|
import { AppNotFound } from '../components';
|
|
17
17
|
import { PluginFlowEngine } from '../flow';
|
|
18
18
|
import {
|
|
19
|
-
ADMIN_LAYOUT_MODEL_UID,
|
|
20
19
|
AdminLayoutMenuItemModel,
|
|
21
20
|
AdminLayoutModel,
|
|
22
21
|
AppSwitcherActionPanelModel,
|
|
23
22
|
} from '../flow/admin-shell/admin-layout';
|
|
24
23
|
import { useApp } from '../hooks/useApp';
|
|
25
24
|
import { Plugin } from '../Plugin';
|
|
25
|
+
import type { PluginClass } from '../PluginManager';
|
|
26
26
|
import { AdminSettingsLayoutModel } from '../settings-center';
|
|
27
|
+
import { SettingsDocumentRedirect } from '../settings-app/SettingsDocumentRedirect';
|
|
28
|
+
import { type CurrentUserAuthStatus, setCurrentUserAuthStatus } from './currentUserAuthStatus';
|
|
27
29
|
import { LocalePlugin } from './plugins/LocalePlugin';
|
|
28
30
|
|
|
29
31
|
export type CurrentUserState = {
|
|
@@ -33,8 +35,6 @@ export type CurrentUserState = {
|
|
|
33
35
|
loading: boolean;
|
|
34
36
|
};
|
|
35
37
|
|
|
36
|
-
type CurrentUserAuthStatus = 'unknown' | 'authenticated' | 'unauthenticated' | 'redirecting';
|
|
37
|
-
|
|
38
38
|
type CurrentUserInternalState = CurrentUserState & {
|
|
39
39
|
authStatus: CurrentUserAuthStatus;
|
|
40
40
|
error?: Error | null;
|
|
@@ -181,6 +181,19 @@ const DataSourceBootstrapProvider: FC = ({ children }) => {
|
|
|
181
181
|
return <>{children}</>;
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
+
function redirectUnauthenticatedRoute(
|
|
185
|
+
app: Application,
|
|
186
|
+
location: { pathname: string; search?: string; hash?: string },
|
|
187
|
+
navigate: ReturnType<typeof useNavigate>,
|
|
188
|
+
) {
|
|
189
|
+
const redirectPath = getCurrentV2RedirectPath(app, location);
|
|
190
|
+
if (app.pluginSettingsManager.getRouteName('') === 'settings.') {
|
|
191
|
+
redirectToV2Signin(app, redirectPath);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
navigate(`/signin?redirect=${encodeURIComponent(redirectPath)}`, { replace: true });
|
|
195
|
+
}
|
|
196
|
+
|
|
184
197
|
const CurrentUserProvider: FC = ({ children }) => {
|
|
185
198
|
const app = useApp<Application>();
|
|
186
199
|
const location = useLocation();
|
|
@@ -234,9 +247,7 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
234
247
|
if (user?.id == null) {
|
|
235
248
|
// 用 react-router navigate (虚拟跳转)而不是 location.replace, 这样如果有其他响应拦截器已经发起了 window.location.href 整页跳转(例如 2FA 插件接收到服务端 302 重定向), 真实跳转可以胜出 navigate, 不会被这里的 signin 重定向覆盖。
|
|
236
249
|
setState({ loading: true, authStatus: 'unauthenticated', error: null });
|
|
237
|
-
|
|
238
|
-
replace: true,
|
|
239
|
-
});
|
|
250
|
+
redirectUnauthenticatedRoute(app, locationRef.current, navigate);
|
|
240
251
|
return;
|
|
241
252
|
}
|
|
242
253
|
|
|
@@ -277,9 +288,7 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
277
288
|
const isAuthError = errorLike?.response?.status === 401 || errorLike?.status === 401;
|
|
278
289
|
if (isAuthError) {
|
|
279
290
|
setState({ loading: true, authStatus: 'unauthenticated', error: null });
|
|
280
|
-
|
|
281
|
-
replace: true,
|
|
282
|
-
});
|
|
291
|
+
redirectUnauthenticatedRoute(app, locationRef.current, navigate);
|
|
283
292
|
return;
|
|
284
293
|
}
|
|
285
294
|
setState({
|
|
@@ -297,6 +306,10 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
297
306
|
};
|
|
298
307
|
}, [app, authCheckRouteState, navigate]);
|
|
299
308
|
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
setCurrentUserAuthStatus(app, state.authStatus);
|
|
311
|
+
}, [app, state.authStatus]);
|
|
312
|
+
|
|
300
313
|
if (state.error) {
|
|
301
314
|
throw state.error;
|
|
302
315
|
}
|
|
@@ -310,19 +323,6 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
310
323
|
|
|
311
324
|
CurrentUserProvider.displayName = 'CurrentUserProvider';
|
|
312
325
|
|
|
313
|
-
const RootRedirect: FC = () => {
|
|
314
|
-
const app = useApp<Application>();
|
|
315
|
-
const hasToken = !!app?.apiClient?.auth?.token;
|
|
316
|
-
const targetPath = getDefaultV2AdminRedirectPath(app);
|
|
317
|
-
|
|
318
|
-
if (!hasToken) {
|
|
319
|
-
// 用 react-router <Navigate /> 而非 location.replace, 避免覆盖同时段其它响应拦截器触发的 window.location.href (例如 2FA 接收到服务端 302 时设置的整页跳转)。
|
|
320
|
-
return <Navigate replace to={`/signin?redirect=${encodeURIComponent(targetPath)}`} />;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return <Navigate replace to="/admin" />;
|
|
324
|
-
};
|
|
325
|
-
|
|
326
326
|
/**
|
|
327
327
|
* client-v2 使用的内建插件集合。
|
|
328
328
|
*
|
|
@@ -346,56 +346,11 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
346
346
|
AppSwitcherActionPanelModel,
|
|
347
347
|
AdminSettingsLayoutModel,
|
|
348
348
|
});
|
|
349
|
-
this.app.layoutManager.registerLayout({
|
|
350
|
-
routeName: 'admin',
|
|
351
|
-
routePath: '/admin',
|
|
352
|
-
uid: ADMIN_LAYOUT_MODEL_UID,
|
|
353
|
-
layoutModelClass: 'AdminLayoutModel',
|
|
354
|
-
});
|
|
355
349
|
|
|
356
|
-
this.app
|
|
357
|
-
key: 'plugin-manager',
|
|
358
|
-
title: this.app.i18n.t('Plugin manager'),
|
|
359
|
-
icon: 'ApiOutlined',
|
|
360
|
-
aclSnippet: 'pm',
|
|
361
|
-
sort: -200,
|
|
362
|
-
});
|
|
363
|
-
this.app.pluginSettingsManager.addPageTabItem({
|
|
364
|
-
menuKey: 'plugin-manager',
|
|
365
|
-
key: 'index',
|
|
366
|
-
title: this.app.i18n.t('Plugin manager'),
|
|
367
|
-
componentLoader: () => import('../settings-center/plugin-manager'),
|
|
368
|
-
aclSnippet: 'pm',
|
|
369
|
-
sort: -200,
|
|
370
|
-
});
|
|
371
|
-
this.app.pluginSettingsManager.addMenuItem({
|
|
372
|
-
key: 'system-settings',
|
|
373
|
-
title: this.app.i18n.t('System settings'),
|
|
374
|
-
icon: 'SettingOutlined',
|
|
375
|
-
aclSnippet: 'pm.system-settings.system-settings',
|
|
376
|
-
});
|
|
377
|
-
this.app.pluginSettingsManager.addPageTabItem({
|
|
378
|
-
menuKey: 'system-settings',
|
|
379
|
-
key: 'index',
|
|
380
|
-
title: this.app.i18n.t('System settings'),
|
|
381
|
-
componentLoader: () => import('../settings-center/SystemSettingsPage'),
|
|
382
|
-
aclSnippet: 'pm.system-settings.system-settings',
|
|
383
|
-
});
|
|
384
|
-
// Parent menu for security-related plugin settings (password policy, locked users, etc.). Registered here in the buildin plugin so any pro plugin can attach page tabs to `menuKey: 'security'` without each one re-registering the same parent.
|
|
385
|
-
this.app.pluginSettingsManager.addMenuItem({
|
|
386
|
-
key: 'security',
|
|
387
|
-
title: this.app.i18n.t('Security'),
|
|
388
|
-
icon: 'SafetyOutlined',
|
|
389
|
-
aclSnippet: 'pm.security',
|
|
390
|
-
});
|
|
350
|
+
registerDefaultSettings(this.app);
|
|
391
351
|
}
|
|
392
352
|
|
|
393
353
|
addRoutes() {
|
|
394
|
-
this.router.add('root', {
|
|
395
|
-
path: '/',
|
|
396
|
-
element: <RootRedirect />,
|
|
397
|
-
});
|
|
398
|
-
|
|
399
354
|
this.router.add('not-found', {
|
|
400
355
|
path: '*',
|
|
401
356
|
Component: AppNotFound,
|
|
@@ -403,7 +358,7 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
403
358
|
|
|
404
359
|
this.router.add('admin.settings', {
|
|
405
360
|
path: '/admin/settings',
|
|
406
|
-
|
|
361
|
+
Component: SettingsDocumentRedirect,
|
|
407
362
|
});
|
|
408
363
|
this.router.add('admin.settings.route-empty', {
|
|
409
364
|
path: '*',
|
|
@@ -419,4 +374,77 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
419
374
|
}
|
|
420
375
|
}
|
|
421
376
|
|
|
377
|
+
function registerDefaultSettings(app: Application) {
|
|
378
|
+
app.pluginSettingsManager.addMenuItem({
|
|
379
|
+
key: 'plugin-manager',
|
|
380
|
+
title: app.i18n.t('Plugin manager'),
|
|
381
|
+
icon: 'ApiOutlined',
|
|
382
|
+
aclSnippet: 'pm',
|
|
383
|
+
sort: -200,
|
|
384
|
+
});
|
|
385
|
+
app.pluginSettingsManager.addPageTabItem({
|
|
386
|
+
menuKey: 'plugin-manager',
|
|
387
|
+
key: 'index',
|
|
388
|
+
title: app.i18n.t('Plugin manager'),
|
|
389
|
+
componentLoader: () => import('../settings-center/plugin-manager'),
|
|
390
|
+
aclSnippet: 'pm',
|
|
391
|
+
sort: -200,
|
|
392
|
+
});
|
|
393
|
+
app.pluginSettingsManager.addMenuItem({
|
|
394
|
+
key: 'system-settings',
|
|
395
|
+
title: app.i18n.t('System settings'),
|
|
396
|
+
icon: 'SettingOutlined',
|
|
397
|
+
aclSnippet: 'pm.system-settings.system-settings',
|
|
398
|
+
});
|
|
399
|
+
app.pluginSettingsManager.addPageTabItem({
|
|
400
|
+
menuKey: 'system-settings',
|
|
401
|
+
key: 'index',
|
|
402
|
+
title: app.i18n.t('System settings'),
|
|
403
|
+
componentLoader: () => import('../settings-center/SystemSettingsPage'),
|
|
404
|
+
aclSnippet: 'pm.system-settings.system-settings',
|
|
405
|
+
});
|
|
406
|
+
// Parent menu for security-related plugin settings (password policy, locked users, etc.). Registered here in the buildin plugin so any pro plugin can attach page tabs to `menuKey: 'security'` without each one re-registering the same parent.
|
|
407
|
+
app.pluginSettingsManager.addMenuItem({
|
|
408
|
+
key: 'security',
|
|
409
|
+
title: app.i18n.t('Security'),
|
|
410
|
+
icon: 'SafetyOutlined',
|
|
411
|
+
aclSnippet: 'pm.security',
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Internal built-in runtime for the standalone Client V2 settings entry.
|
|
417
|
+
* It intentionally shares the existing plugin lane and runtime providers but
|
|
418
|
+
* does not register the Admin Layout.
|
|
419
|
+
*/
|
|
420
|
+
export class SettingsBuildInPlugin extends Plugin<any, Application> {
|
|
421
|
+
async afterAdd() {
|
|
422
|
+
await this.app.pm.add(PluginFlowEngine);
|
|
423
|
+
await this.app.pm.add(LocalePlugin as unknown as PluginClass, { name: 'builtin-locale' });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async load() {
|
|
427
|
+
this.app.use(CurrentUserProvider);
|
|
428
|
+
this.app.use(DataSourceBootstrapProvider);
|
|
429
|
+
this.app.flowEngine.registerModels({ AdminSettingsLayoutModel });
|
|
430
|
+
|
|
431
|
+
this.router.add('settings', {
|
|
432
|
+
path: '/settings',
|
|
433
|
+
authCheck: true,
|
|
434
|
+
componentLoader: () => import('../settings-center/AdminSettingsLayout'),
|
|
435
|
+
});
|
|
436
|
+
this.router.add('settingsDetails', {
|
|
437
|
+
path: '/settings',
|
|
438
|
+
authCheck: true,
|
|
439
|
+
Component: Outlet,
|
|
440
|
+
});
|
|
441
|
+
this.router.add('settings.route-empty', {
|
|
442
|
+
path: '*',
|
|
443
|
+
Component: Outlet,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
registerDefaultSettings(this.app);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
422
450
|
export { NocoBaseBuildInPlugin as NocoBaseBuildInPluginV2 };
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { Application, type ApplicationOptions } from '../index';
|
|
11
|
+
import { SettingsPluginSettingsManager } from './SettingsPluginSettingsManager';
|
|
12
|
+
import { SettingsRouterManager } from './SettingsRouterManager';
|
|
13
|
+
import { SettingsShell } from './SettingsShell';
|
|
14
|
+
|
|
15
|
+
export class SettingsApplication extends Application {
|
|
16
|
+
/** 设置中心的标记,插件通过 `isSettingsApp(app)` 读它,避免反向依赖这个类。 */
|
|
17
|
+
readonly isSettingsApp = true;
|
|
18
|
+
|
|
19
|
+
protected addCustomProviders() {
|
|
20
|
+
super.addCustomProviders();
|
|
21
|
+
this.use(SettingsShell);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected createRouterManager(options: ApplicationOptions): SettingsRouterManager<this> {
|
|
25
|
+
return new SettingsRouterManager<this>(options.router, this);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected createPluginSettingsManager(_options: ApplicationOptions): SettingsPluginSettingsManager<this> {
|
|
29
|
+
return new SettingsPluginSettingsManager<this>(this);
|
|
30
|
+
}
|
|
31
|
+
}
|