@nocobase/client-v2 2.2.0-beta.9 → 3.0.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.
Files changed (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -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,17 @@ 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);
251
+ return;
252
+ }
253
+
254
+ try {
255
+ await app.apiClient.auth.syncCookies();
256
+ } catch {
257
+ // Cookie bootstrap is best-effort; auth:check remains the source of truth for the current page load.
258
+ }
259
+
260
+ if (!mounted) {
240
261
  return;
241
262
  }
242
263
 
@@ -267,9 +288,7 @@ const CurrentUserProvider: FC = ({ children }) => {
267
288
  const isAuthError = errorLike?.response?.status === 401 || errorLike?.status === 401;
268
289
  if (isAuthError) {
269
290
  setState({ loading: true, authStatus: 'unauthenticated', error: null });
270
- navigate(`/signin?redirect=${encodeURIComponent(getCurrentV2RedirectPath(app, locationRef.current))}`, {
271
- replace: true,
272
- });
291
+ redirectUnauthenticatedRoute(app, locationRef.current, navigate);
273
292
  return;
274
293
  }
275
294
  setState({
@@ -287,6 +306,10 @@ const CurrentUserProvider: FC = ({ children }) => {
287
306
  };
288
307
  }, [app, authCheckRouteState, navigate]);
289
308
 
309
+ useEffect(() => {
310
+ setCurrentUserAuthStatus(app, state.authStatus);
311
+ }, [app, state.authStatus]);
312
+
290
313
  if (state.error) {
291
314
  throw state.error;
292
315
  }
@@ -298,18 +321,7 @@ const CurrentUserProvider: FC = ({ children }) => {
298
321
  return <CurrentUserContext.Provider value={contextValue}>{children}</CurrentUserContext.Provider>;
299
322
  };
300
323
 
301
- const RootRedirect: FC = () => {
302
- const app = useApp<Application>();
303
- const hasToken = !!app?.apiClient?.auth?.token;
304
- const targetPath = getDefaultV2AdminRedirectPath(app);
305
-
306
- if (!hasToken) {
307
- // 用 react-router <Navigate /> 而非 location.replace, 避免覆盖同时段其它响应拦截器触发的 window.location.href (例如 2FA 接收到服务端 302 时设置的整页跳转)。
308
- return <Navigate replace to={`/signin?redirect=${encodeURIComponent(targetPath)}`} />;
309
- }
310
-
311
- return <Navigate replace to="/admin" />;
312
- };
324
+ CurrentUserProvider.displayName = 'CurrentUserProvider';
313
325
 
314
326
  /**
315
327
  * client-v2 使用的内建插件集合。
@@ -334,56 +346,11 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
334
346
  AppSwitcherActionPanelModel,
335
347
  AdminSettingsLayoutModel,
336
348
  });
337
- this.app.layoutManager.registerLayout({
338
- routeName: 'admin',
339
- routePath: '/admin',
340
- uid: ADMIN_LAYOUT_MODEL_UID,
341
- layoutModelClass: 'AdminLayoutModel',
342
- });
343
349
 
344
- this.app.pluginSettingsManager.addMenuItem({
345
- key: 'plugin-manager',
346
- title: this.app.i18n.t('Plugin manager'),
347
- icon: 'ApiOutlined',
348
- aclSnippet: 'pm',
349
- sort: -200,
350
- });
351
- this.app.pluginSettingsManager.addPageTabItem({
352
- menuKey: 'plugin-manager',
353
- key: 'index',
354
- title: this.app.i18n.t('Plugin manager'),
355
- componentLoader: () => import('../settings-center/plugin-manager'),
356
- aclSnippet: 'pm',
357
- sort: -200,
358
- });
359
- this.app.pluginSettingsManager.addMenuItem({
360
- key: 'system-settings',
361
- title: this.app.i18n.t('System settings'),
362
- icon: 'SettingOutlined',
363
- aclSnippet: 'pm.system-settings.system-settings',
364
- });
365
- this.app.pluginSettingsManager.addPageTabItem({
366
- menuKey: 'system-settings',
367
- key: 'index',
368
- title: this.app.i18n.t('System settings'),
369
- componentLoader: () => import('../settings-center/SystemSettingsPage'),
370
- aclSnippet: 'pm.system-settings.system-settings',
371
- });
372
- // 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.
373
- this.app.pluginSettingsManager.addMenuItem({
374
- key: 'security',
375
- title: this.app.i18n.t('Security'),
376
- icon: 'SafetyOutlined',
377
- aclSnippet: 'pm.security',
378
- });
350
+ registerDefaultSettings(this.app);
379
351
  }
380
352
 
381
353
  addRoutes() {
382
- this.router.add('root', {
383
- path: '/',
384
- element: <RootRedirect />,
385
- });
386
-
387
354
  this.router.add('not-found', {
388
355
  path: '*',
389
356
  Component: AppNotFound,
@@ -391,7 +358,7 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
391
358
 
392
359
  this.router.add('admin.settings', {
393
360
  path: '/admin/settings',
394
- componentLoader: () => import('../settings-center/AdminSettingsLayout'),
361
+ Component: SettingsDocumentRedirect,
395
362
  });
396
363
  this.router.add('admin.settings.route-empty', {
397
364
  path: '*',
@@ -407,4 +374,77 @@ export class NocoBaseBuildInPlugin extends Plugin<any, Application> {
407
374
  }
408
375
  }
409
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
+
410
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
+ }
@@ -0,0 +1,108 @@
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 { css } from '@emotion/css';
11
+ import { observer } from '@nocobase/flow-engine';
12
+ import { theme as antdTheme } from 'antd';
13
+ import React from 'react';
14
+ import { useTranslation } from 'react-i18next';
15
+ import { useSystemSettings } from '../flow/system-settings';
16
+ import { getSettingsHeaderColors } from './settingsTheme';
17
+
18
+ const brandClassName = css`
19
+ display: inline-flex;
20
+ align-items: center;
21
+ gap: 8px;
22
+ height: 100%;
23
+ flex-shrink: 0;
24
+ min-width: 0;
25
+ `;
26
+
27
+ const logoImageClassName = css`
28
+ height: 24px;
29
+ width: auto;
30
+ max-width: 140px;
31
+ object-fit: contain;
32
+ display: block;
33
+ `;
34
+
35
+ const titleClassName = css`
36
+ font-weight: 600;
37
+ line-height: 1;
38
+ white-space: nowrap;
39
+ overflow: hidden;
40
+ text-overflow: ellipsis;
41
+ `;
42
+
43
+ const suffixClassName = css`
44
+ line-height: 1;
45
+ white-space: nowrap;
46
+ font-weight: 400;
47
+ `;
48
+
49
+ /**
50
+ * 设置中心顶栏品牌区。
51
+ *
52
+ * 沿用系统设置里的站点 logo / 标题,并在其后补一个弱化的「设置中心」副标题,
53
+ * 用于区分设置中心和业务端。
54
+ */
55
+ export const SettingsBrand = observer(() => {
56
+ const { token } = antdTheme.useToken();
57
+ const headerColors = getSettingsHeaderColors(token);
58
+ const result = useSystemSettings();
59
+ const { t } = useTranslation();
60
+ const { t: tCollections } = useTranslation('lm-collections');
61
+ const title = tCollections(result?.data?.data?.title || 'NocoBase');
62
+ const logoUrl = result?.data?.data?.logo?.url;
63
+
64
+ if (result?.loading) {
65
+ return <div className={brandClassName} />;
66
+ }
67
+
68
+ return (
69
+ <div className={brandClassName}>
70
+ {logoUrl ? (
71
+ // 站点 logo 通常是为深色顶栏画的浅色图;设置中心顶栏是灰白的,
72
+ // 统一压成单色,避免浅色 logo 直接消失在白底上。
73
+ <img
74
+ alt={title}
75
+ className={logoImageClassName}
76
+ src={logoUrl}
77
+ style={{ filter: headerColors.dark ? 'brightness(0) invert(1)' : 'brightness(0)' }}
78
+ />
79
+ ) : (
80
+ <span className={titleClassName} style={{ color: headerColors.text, fontSize: token.fontSizeHeading4 }}>
81
+ {title}
82
+ </span>
83
+ )}
84
+ <span
85
+ aria-hidden
86
+ style={{
87
+ alignSelf: 'center',
88
+ background: headerColors.text,
89
+ height: 16,
90
+ opacity: 0.3,
91
+ width: 1,
92
+ }}
93
+ />
94
+ <span
95
+ className={suffixClassName}
96
+ style={{
97
+ color: headerColors.text,
98
+ fontSize: token.fontSize,
99
+ opacity: 0.75,
100
+ }}
101
+ >
102
+ {t('Settings center')}
103
+ </span>
104
+ </div>
105
+ );
106
+ });
107
+
108
+ SettingsBrand.displayName = 'SettingsBrand';
@@ -0,0 +1,10 @@
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
+ export { SettingsBuildInPlugin } from '../nocobase-buildin-plugin';
@@ -0,0 +1,26 @@
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 React, { useEffect } from 'react';
11
+ import { useLocation } from 'react-router-dom';
12
+ import { useApp } from '../hooks/useApp';
13
+ import { resolveStandaloneSettingsPath } from './runtimePaths';
14
+
15
+ export function SettingsDocumentRedirect() {
16
+ const app = useApp();
17
+ const location = useLocation();
18
+ const sourcePath = `${location.pathname}${location.search}${location.hash}`;
19
+ const targetPath = resolveStandaloneSettingsPath(app, sourcePath, location.pathname);
20
+
21
+ useEffect(() => {
22
+ window.location.replace(targetPath);
23
+ }, [targetPath]);
24
+
25
+ return app.renderComponent('AppSpin');
26
+ }
@@ -0,0 +1,124 @@
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 { observer } from '@nocobase/flow-engine';
11
+ import { ConfigProvider, Menu, theme as antdTheme } from 'antd';
12
+ import React, { useCallback, useMemo } from 'react';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { useLocation, useNavigate } from 'react-router-dom';
15
+ import { useApp } from '../hooks/useApp';
16
+ import { useSettingsGroups } from '../settings-center/useSettingsGroups';
17
+ import { buildSettingsHeaderMenuTheme } from './settingsTheme';
18
+
19
+ const navStyle: React.CSSProperties = {
20
+ background: 'transparent',
21
+ borderBottom: 'none',
22
+ flex: 'auto',
23
+ minWidth: 0,
24
+ lineHeight: '46px',
25
+ };
26
+
27
+ /**
28
+ * 设置中心顶栏的一级导航。
29
+ *
30
+ * 应用、插件管理各自只有一个页面,直接当一级入口;其余全部收在「系统设置」下,
31
+ * 鼠标移上去展开下拉,也可以点进去从左栏走。
32
+ */
33
+ export const SettingsGroupNav: React.FC = observer(() => {
34
+ const { t } = useTranslation();
35
+ const app = useApp();
36
+ const navigate = useNavigate();
37
+ const location = useLocation();
38
+ const { token } = antdTheme.useToken();
39
+ const { groups, activeGroupKey, currentTopLevelSetting, getGroupEntryPath } = useSettingsGroups();
40
+ const headerMenuTheme = useMemo(() => buildSettingsHeaderMenuTheme(token), [token]);
41
+ // 登录 / 找回密码等免鉴权页面共用同一个 shell,这些页面上不该出现设置导航。
42
+ const isAuthRoute = app.router.isSkippedAuthCheckRoute(location.pathname);
43
+
44
+ const handleClick = useCallback(
45
+ ({ key }: { key: string }) => {
46
+ const [groupKey, settingName] = key.split('::');
47
+ const targetPath = settingName
48
+ ? groups.find((group) => group.key === groupKey)?.settings.find((item) => item.name === settingName)?.path
49
+ : getGroupEntryPath(groupKey);
50
+
51
+ if (targetPath && targetPath !== location.pathname) {
52
+ navigate(targetPath);
53
+ }
54
+ },
55
+ [getGroupEntryPath, groups, location.pathname, navigate],
56
+ );
57
+
58
+ // 单入口分组直接当一级项,标题取配置项自己的名字(顺带继承插件的翻译);
59
+ // 系统设置这种大分组挂上子项,鼠标移上去展开。
60
+ const items = useMemo(
61
+ () =>
62
+ groups.map((group) => {
63
+ if (group.settings.length <= 1) {
64
+ return {
65
+ key: group.key,
66
+ label: group.settings[0]?.label ?? t(group.title),
67
+ icon: group.settings[0]?.icon,
68
+ };
69
+ }
70
+
71
+ // 已经站在这个分组里时不再弹下拉:左栏已经把同一份内容铺开了,再浮一层纯属打扰。
72
+ if (group.key === activeGroupKey) {
73
+ return { key: group.key, label: t(group.title) };
74
+ }
75
+
76
+ return {
77
+ key: group.key,
78
+ label: t(group.title),
79
+ children: group.settings
80
+ .filter((setting) => !setting.hidden)
81
+ .map((setting) => ({
82
+ key: `${group.key}::${setting.name}`,
83
+ label: setting.label ?? setting.title,
84
+ icon: setting.icon,
85
+ })),
86
+ // 点标题本身也要能进去,而不是只能从下拉里挑。
87
+ onTitleClick: handleClick,
88
+ };
89
+ }),
90
+ [activeGroupKey, groups, handleClick, t],
91
+ );
92
+
93
+ // 子菜单标题只有在它的某个子项被选中时才会高亮,所以两个 key 都要给。
94
+ const selectedKeys = useMemo(() => {
95
+ if (!activeGroupKey) {
96
+ return [];
97
+ }
98
+ return currentTopLevelSetting?.name
99
+ ? [activeGroupKey, `${activeGroupKey}::${currentTopLevelSetting.name}`]
100
+ : [activeGroupKey];
101
+ }, [activeGroupKey, currentTopLevelSetting?.name]);
102
+
103
+ if (isAuthRoute || items.length <= 1) {
104
+ return <div style={{ flex: 'auto' }} />;
105
+ }
106
+
107
+ return (
108
+ <ConfigProvider theme={headerMenuTheme}>
109
+ <Menu
110
+ mode="horizontal"
111
+ disabledOverflow={false}
112
+ selectedKeys={selectedKeys}
113
+ items={items}
114
+ style={navStyle}
115
+ triggerSubMenuAction="hover"
116
+ onClick={handleClick}
117
+ />
118
+ </ConfigProvider>
119
+ );
120
+ });
121
+
122
+ SettingsGroupNav.displayName = 'SettingsGroupNav';
123
+
124
+ export default SettingsGroupNav;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import type { BaseApplication } from '../BaseApplication';
11
+ import { PluginSettingsManager } from '../PluginSettingsManager';
12
+ import { resolveSettingsAppScopeWithinPublicPath } from './settingsDocumentPath';
13
+
14
+ const SETTINGS_ROUTE_PREFIX = 'settings.';
15
+ const MAIN_SETTINGS_PATH_PREFIX = '/settings/';
16
+
17
+ export class SettingsPluginSettingsManager<
18
+ TApp extends BaseApplication<any> = BaseApplication<any>,
19
+ > extends PluginSettingsManager<TApp> {
20
+ getRouteName(name: string) {
21
+ return `${SETTINGS_ROUTE_PREFIX}${name}`;
22
+ }
23
+
24
+ getRoutePath(name: string) {
25
+ const appScope = resolveSettingsAppScopeWithinPublicPath(this.app.getPublicPath(), this.app.router.getBasename?.());
26
+ const pathPrefix = appScope ? '/' : MAIN_SETTINGS_PATH_PREFIX;
27
+ const separatorIndex = name.indexOf('.');
28
+ const menuName = separatorIndex < 0 ? name : name.slice(0, separatorIndex);
29
+ const pageName = separatorIndex < 0 ? undefined : name.slice(separatorIndex + 1);
30
+ const menuPath = `${pathPrefix}${menuName}`;
31
+
32
+ if (!pageName || pageName === 'index') {
33
+ return menuPath;
34
+ }
35
+
36
+ return `${menuPath}/${pageName}`;
37
+ }
38
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import type { BaseApplication } from '../BaseApplication';
11
+ import { RouterManager, type RouteType } from '../RouterManager';
12
+ import { resolveSettingsAppScopeWithinPublicPath } from './settingsDocumentPath';
13
+
14
+ function isSettingsOwnedRoute(name: string) {
15
+ return (
16
+ name === 'not-found' ||
17
+ name === 'settings' ||
18
+ name.startsWith('settings.') ||
19
+ name === 'settingsDetails' ||
20
+ name.startsWith('settingsDetails.') ||
21
+ name === 'auth' ||
22
+ name.startsWith('auth.') ||
23
+ name === '2fa' ||
24
+ name.startsWith('2fa.')
25
+ );
26
+ }
27
+
28
+ function isSettingsAuthenticationRoute(name: string) {
29
+ return name === 'auth' || name.startsWith('auth.') || name === '2fa' || name.startsWith('2fa.');
30
+ }
31
+
32
+ export class SettingsRouterManager<
33
+ TApp extends BaseApplication<any> = BaseApplication<any>,
34
+ > extends RouterManager<TApp> {
35
+ private rebaseScopedSettingsRoute(route: RouteType) {
36
+ const appScope = resolveSettingsAppScopeWithinPublicPath(this.app.getPublicPath(), this.getBasename());
37
+ if (!route.path?.startsWith('/') || !appScope) {
38
+ return route;
39
+ }
40
+ if (route.path === '/settings') {
41
+ return {
42
+ ...route,
43
+ path: '/',
44
+ };
45
+ }
46
+ if (!route.path.startsWith('/settings/')) {
47
+ return route;
48
+ }
49
+ return {
50
+ ...route,
51
+ path: route.path.slice('/settings'.length),
52
+ };
53
+ }
54
+
55
+ private rebaseAuthenticationRoute(route: RouteType) {
56
+ if (!route.path?.startsWith('/')) {
57
+ return route;
58
+ }
59
+
60
+ const settingsRoot = this.app.pluginSettingsManager.getRoutePath('').replace(/\/+$/, '');
61
+ if (!settingsRoot) {
62
+ return route;
63
+ }
64
+ if (route.path === settingsRoot || route.path.startsWith(`${settingsRoot}/`)) {
65
+ return route;
66
+ }
67
+
68
+ return {
69
+ ...route,
70
+ path: `${settingsRoot}/${route.path.replace(/^\/+/, '')}`,
71
+ };
72
+ }
73
+
74
+ add(name: string, route: RouteType) {
75
+ if (!isSettingsOwnedRoute(name)) {
76
+ return;
77
+ }
78
+
79
+ const scopedRoute = this.rebaseScopedSettingsRoute(route);
80
+ const ownedRoute = isSettingsAuthenticationRoute(name) ? this.rebaseAuthenticationRoute(scopedRoute) : scopedRoute;
81
+
82
+ super.add(
83
+ name,
84
+ name === 'settingsDetails' || name.startsWith('settingsDetails.')
85
+ ? {
86
+ ...ownedRoute,
87
+ authCheck: true,
88
+ }
89
+ : ownedRoute,
90
+ );
91
+ }
92
+ }