@nocobase/client-v2 2.3.0-beta.6 → 2.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +11 -2
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/index.d.ts +4 -1
- package/es/index.mjs +129 -118
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +10 -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 +18 -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 +157 -146
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/authRedirect.test.ts +154 -0
- package/src/__tests__/browserChecker.test.ts +172 -6
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
- 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 +168 -130
- package/src/__tests__/settings-layout-root.test.tsx +600 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +261 -0
- package/src/acl/ACLProvider.tsx +53 -3
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +85 -8
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/index.ts +1 -0
- 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/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/index.ts +11 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +112 -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 +343 -0
- package/src/settings-app/SettingsShell.tsx +184 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +103 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +369 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +154 -169
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- 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
|
}
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,15 @@ 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
|
+
resolveUnauthenticatedSignInRoute,
|
|
37
|
+
useCurrentRoles,
|
|
38
|
+
useCurrentUserContext,
|
|
39
|
+
} from './nocobase-buildin-plugin';
|
|
40
|
+
export type { CurrentRoleOption, CurrentUserState } from './nocobase-buildin-plugin';
|
|
33
41
|
export { getRouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
|
|
34
42
|
export type { RouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
|
|
35
43
|
export * from './collection-field-interface/CollectionFieldInterface';
|
|
@@ -55,3 +63,5 @@ export type { NocoBaseDesktopRoute } from './flow-compat';
|
|
|
55
63
|
export * from './utils/markdownSanitize';
|
|
56
64
|
export * from './ui-operation';
|
|
57
65
|
export { default as AntdAppProvider } from './theme/AntdAppProvider';
|
|
66
|
+
export { isSettingsApp } from './settings-app/isSettingsApp';
|
|
67
|
+
export { MINIMAL_THEME_UID, useSettingsThemeConfig } from './settings-app/useSettingsThemeConfig';
|
|
@@ -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,32 @@ const DataSourceBootstrapProvider: FC = ({ children }) => {
|
|
|
181
181
|
return <>{children}</>;
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
+
export function resolveUnauthenticatedSignInRoute(app: Application, pathname: string) {
|
|
185
|
+
const matchedRoutes = app.router.matchRoutes(pathname) || [];
|
|
186
|
+
for (let index = matchedRoutes.length - 1; index >= 0; index -= 1) {
|
|
187
|
+
const pathnameBase = matchedRoutes[index].pathnameBase.replace(/\/+$/g, '');
|
|
188
|
+
const candidate = `${pathnameBase}/signin`;
|
|
189
|
+
if (app.router.isSkippedAuthCheckRoute(candidate)) {
|
|
190
|
+
return candidate;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return '/signin';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function redirectUnauthenticatedRoute(
|
|
197
|
+
app: Application,
|
|
198
|
+
location: { pathname: string; search?: string; hash?: string },
|
|
199
|
+
navigate: ReturnType<typeof useNavigate>,
|
|
200
|
+
) {
|
|
201
|
+
const redirectPath = getCurrentV2RedirectPath(app, location);
|
|
202
|
+
if (app.pluginSettingsManager.getRouteName('') === 'settings.') {
|
|
203
|
+
redirectToV2Signin(app, redirectPath);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const signInPath = resolveUnauthenticatedSignInRoute(app, location.pathname);
|
|
207
|
+
navigate(`${signInPath}?redirect=${encodeURIComponent(redirectPath)}`, { replace: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
184
210
|
const CurrentUserProvider: FC = ({ children }) => {
|
|
185
211
|
const app = useApp<Application>();
|
|
186
212
|
const location = useLocation();
|
|
@@ -234,9 +260,7 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
234
260
|
if (user?.id == null) {
|
|
235
261
|
// 用 react-router navigate (虚拟跳转)而不是 location.replace, 这样如果有其他响应拦截器已经发起了 window.location.href 整页跳转(例如 2FA 插件接收到服务端 302 重定向), 真实跳转可以胜出 navigate, 不会被这里的 signin 重定向覆盖。
|
|
236
262
|
setState({ loading: true, authStatus: 'unauthenticated', error: null });
|
|
237
|
-
|
|
238
|
-
replace: true,
|
|
239
|
-
});
|
|
263
|
+
redirectUnauthenticatedRoute(app, locationRef.current, navigate);
|
|
240
264
|
return;
|
|
241
265
|
}
|
|
242
266
|
|
|
@@ -277,9 +301,7 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
277
301
|
const isAuthError = errorLike?.response?.status === 401 || errorLike?.status === 401;
|
|
278
302
|
if (isAuthError) {
|
|
279
303
|
setState({ loading: true, authStatus: 'unauthenticated', error: null });
|
|
280
|
-
|
|
281
|
-
replace: true,
|
|
282
|
-
});
|
|
304
|
+
redirectUnauthenticatedRoute(app, locationRef.current, navigate);
|
|
283
305
|
return;
|
|
284
306
|
}
|
|
285
307
|
setState({
|
|
@@ -297,6 +319,10 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
297
319
|
};
|
|
298
320
|
}, [app, authCheckRouteState, navigate]);
|
|
299
321
|
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
setCurrentUserAuthStatus(app, state.authStatus);
|
|
324
|
+
}, [app, state.authStatus]);
|
|
325
|
+
|
|
300
326
|
if (state.error) {
|
|
301
327
|
throw state.error;
|
|
302
328
|
}
|
|
@@ -310,19 +336,6 @@ const CurrentUserProvider: FC = ({ children }) => {
|
|
|
310
336
|
|
|
311
337
|
CurrentUserProvider.displayName = 'CurrentUserProvider';
|
|
312
338
|
|
|
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
339
|
/**
|
|
327
340
|
* client-v2 使用的内建插件集合。
|
|
328
341
|
*
|
|
@@ -346,56 +359,11 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
346
359
|
AppSwitcherActionPanelModel,
|
|
347
360
|
AdminSettingsLayoutModel,
|
|
348
361
|
});
|
|
349
|
-
this.app.layoutManager.registerLayout({
|
|
350
|
-
routeName: 'admin',
|
|
351
|
-
routePath: '/admin',
|
|
352
|
-
uid: ADMIN_LAYOUT_MODEL_UID,
|
|
353
|
-
layoutModelClass: 'AdminLayoutModel',
|
|
354
|
-
});
|
|
355
362
|
|
|
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
|
-
});
|
|
363
|
+
registerDefaultSettings(this.app);
|
|
391
364
|
}
|
|
392
365
|
|
|
393
366
|
addRoutes() {
|
|
394
|
-
this.router.add('root', {
|
|
395
|
-
path: '/',
|
|
396
|
-
element: <RootRedirect />,
|
|
397
|
-
});
|
|
398
|
-
|
|
399
367
|
this.router.add('not-found', {
|
|
400
368
|
path: '*',
|
|
401
369
|
Component: AppNotFound,
|
|
@@ -403,7 +371,7 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
403
371
|
|
|
404
372
|
this.router.add('admin.settings', {
|
|
405
373
|
path: '/admin/settings',
|
|
406
|
-
|
|
374
|
+
Component: SettingsDocumentRedirect,
|
|
407
375
|
});
|
|
408
376
|
this.router.add('admin.settings.route-empty', {
|
|
409
377
|
path: '*',
|
|
@@ -419,4 +387,77 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
|
|
|
419
387
|
}
|
|
420
388
|
}
|
|
421
389
|
|
|
390
|
+
function registerDefaultSettings(app: Application) {
|
|
391
|
+
app.pluginSettingsManager.addMenuItem({
|
|
392
|
+
key: 'plugin-manager',
|
|
393
|
+
title: app.i18n.t('Plugin manager'),
|
|
394
|
+
icon: 'ApiOutlined',
|
|
395
|
+
aclSnippet: 'pm',
|
|
396
|
+
sort: -200,
|
|
397
|
+
});
|
|
398
|
+
app.pluginSettingsManager.addPageTabItem({
|
|
399
|
+
menuKey: 'plugin-manager',
|
|
400
|
+
key: 'index',
|
|
401
|
+
title: app.i18n.t('Plugin manager'),
|
|
402
|
+
componentLoader: () => import('../settings-center/plugin-manager'),
|
|
403
|
+
aclSnippet: 'pm',
|
|
404
|
+
sort: -200,
|
|
405
|
+
});
|
|
406
|
+
app.pluginSettingsManager.addMenuItem({
|
|
407
|
+
key: 'system-settings',
|
|
408
|
+
title: app.i18n.t('System settings'),
|
|
409
|
+
icon: 'SettingOutlined',
|
|
410
|
+
aclSnippet: 'pm.system-settings.system-settings',
|
|
411
|
+
});
|
|
412
|
+
app.pluginSettingsManager.addPageTabItem({
|
|
413
|
+
menuKey: 'system-settings',
|
|
414
|
+
key: 'index',
|
|
415
|
+
title: app.i18n.t('System settings'),
|
|
416
|
+
componentLoader: () => import('../settings-center/SystemSettingsPage'),
|
|
417
|
+
aclSnippet: 'pm.system-settings.system-settings',
|
|
418
|
+
});
|
|
419
|
+
// 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.
|
|
420
|
+
app.pluginSettingsManager.addMenuItem({
|
|
421
|
+
key: 'security',
|
|
422
|
+
title: app.i18n.t('Security'),
|
|
423
|
+
icon: 'SafetyOutlined',
|
|
424
|
+
aclSnippet: 'pm.security',
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Internal built-in runtime for the standalone Client V2 settings entry.
|
|
430
|
+
* It intentionally shares the existing plugin lane and runtime providers but
|
|
431
|
+
* does not register the Admin Layout.
|
|
432
|
+
*/
|
|
433
|
+
export class SettingsBuildInPlugin extends Plugin<any, Application> {
|
|
434
|
+
async afterAdd() {
|
|
435
|
+
await this.app.pm.add(PluginFlowEngine);
|
|
436
|
+
await this.app.pm.add(LocalePlugin as unknown as PluginClass, { name: 'builtin-locale' });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async load() {
|
|
440
|
+
this.app.use(CurrentUserProvider);
|
|
441
|
+
this.app.use(DataSourceBootstrapProvider);
|
|
442
|
+
this.app.flowEngine.registerModels({ AdminSettingsLayoutModel });
|
|
443
|
+
|
|
444
|
+
this.router.add('settings', {
|
|
445
|
+
path: '/settings',
|
|
446
|
+
authCheck: true,
|
|
447
|
+
componentLoader: () => import('../settings-center/AdminSettingsLayout'),
|
|
448
|
+
});
|
|
449
|
+
this.router.add('settingsDetails', {
|
|
450
|
+
path: '/settings',
|
|
451
|
+
authCheck: true,
|
|
452
|
+
Component: Outlet,
|
|
453
|
+
});
|
|
454
|
+
this.router.add('settings.route-empty', {
|
|
455
|
+
path: '*',
|
|
456
|
+
Component: Outlet,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
registerDefaultSettings(this.app);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
422
463
|
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
|
+
}
|