@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.
Files changed (123) hide show
  1. package/es/acl/ACLProvider.d.ts +10 -0
  2. package/es/authRedirect.d.ts +5 -0
  3. package/es/components/AttachmentUpload.d.ts +51 -0
  4. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/actions/linkageRules.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  8. package/es/flow/components/FlowRoute.d.ts +2 -2
  9. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  10. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  11. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  12. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  13. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  14. package/es/index.d.ts +4 -1
  15. package/es/index.mjs +139 -125
  16. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  17. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  18. package/es/settings-app/SettingsApplication.d.ts +18 -0
  19. package/es/settings-app/SettingsBrand.d.ts +16 -0
  20. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  21. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  22. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  23. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  24. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  25. package/es/settings-app/SettingsSearch.d.ts +17 -0
  26. package/es/settings-app/SettingsShell.d.ts +10 -0
  27. package/es/settings-app/isSettingsApp.d.ts +18 -0
  28. package/es/settings-app/runtimePaths.d.ts +18 -0
  29. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  30. package/es/settings-app/settingsTheme.d.ts +87 -0
  31. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  32. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  33. package/es/settings-center/groups.d.ts +66 -0
  34. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  35. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  36. package/es/settings-center/utils.d.ts +24 -0
  37. package/es/theme/index.d.ts +1 -0
  38. package/es/theme/itemActive.d.ts +24 -0
  39. package/es/theme/type.d.ts +9 -0
  40. package/lib/index.js +141 -127
  41. package/lib/locale/languageCodes.js +1 -0
  42. package/package.json +8 -7
  43. package/src/__tests__/authRedirect.test.ts +137 -0
  44. package/src/__tests__/browserChecker.test.ts +124 -6
  45. package/src/__tests__/mockSettingsApplication.ts +29 -0
  46. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +51 -10
  47. package/src/__tests__/plugin-manager.test.tsx +7 -6
  48. package/src/__tests__/settings-application.test.ts +164 -0
  49. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  50. package/src/__tests__/settings-center.test.tsx +38 -37
  51. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  52. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  53. package/src/__tests__/settings-shell.test.tsx +200 -0
  54. package/src/acl/ACLProvider.tsx +21 -0
  55. package/src/authRedirect.ts +72 -3
  56. package/src/components/AttachmentUpload.tsx +275 -0
  57. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  58. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  59. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  60. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  61. package/src/components/index.ts +1 -0
  62. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  63. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  64. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  65. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  66. package/src/flow/actions/linkageRules.tsx +26 -6
  67. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
  68. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  69. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FlowRoute.tsx +71 -29
  74. package/src/flow/index.ts +2 -0
  75. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  76. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  77. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  78. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  79. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  80. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  81. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  82. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  83. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  84. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  85. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  86. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  87. package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
  88. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  89. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  90. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  91. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  92. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  93. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  94. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  95. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  96. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  97. package/src/index.ts +10 -1
  98. package/src/locale/languageCodes.ts +1 -0
  99. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  100. package/src/nocobase-buildin-plugin/index.tsx +99 -71
  101. package/src/settings-app/SettingsApplication.ts +31 -0
  102. package/src/settings-app/SettingsBrand.tsx +108 -0
  103. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  104. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  105. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  106. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  107. package/src/settings-app/SettingsRouterManager.ts +92 -0
  108. package/src/settings-app/SettingsSearch.tsx +238 -0
  109. package/src/settings-app/SettingsShell.tsx +167 -0
  110. package/src/settings-app/isSettingsApp.ts +21 -0
  111. package/src/settings-app/runtimePaths.ts +104 -0
  112. package/src/settings-app/settingsDocumentPath.ts +66 -0
  113. package/src/settings-app/settingsTheme.ts +301 -0
  114. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  115. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  116. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  117. package/src/settings-center/groups.ts +108 -0
  118. package/src/settings-center/useSettingsGroups.ts +105 -0
  119. package/src/settings-center/useSettingsSearch.ts +173 -0
  120. package/src/settings-center/utils.tsx +50 -1
  121. package/src/theme/index.tsx +1 -0
  122. package/src/theme/itemActive.ts +31 -0
  123. 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 { Link, useLocation } from 'react-router-dom';
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 href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
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 pluginManagerSetting = topLevelSettings.find((item) => item.name === PLUGIN_MANAGER_SETTING_NAME);
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 orderedSettings = (getMenuItems(normalSettings) || []).map((item: any) => {
257
- if (item?.type === 'divider') {
258
- return item;
259
- }
260
-
261
- const matchedSetting = settingsByKey.get(String(item.key));
262
- const targetPath = matchedSetting?.path;
263
- const targetLink = matchedSetting?.link;
264
- const targetTitle = matchedSetting?.title || item.title;
265
-
266
- return {
267
- key: item.key,
268
- name: matchedSetting?.name,
269
- path: matchedSetting?.path,
270
- link: targetLink,
271
- title: targetTitle,
272
- icon: item.icon,
273
- label: targetLink ? (
274
- <TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
275
- ) : (
276
- <TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
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
- if (canManagePlugins && orderedSettings.length) {
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(...orderedSettings);
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 * from './nocobase-buildin-plugin';
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 { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
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, getDefaultV2AdminRedirectPath } from '../authRedirect';
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
- navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
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
- navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
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.pluginSettingsManager.addMenuItem({
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
- componentLoader: () => import('../settings-center/AdminSettingsLayout'),
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
+ }