@nocobase/client-v2 2.1.0-beta.37 → 2.1.0-beta.38

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/Application.d.ts +1 -0
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/components/KeepAlive.d.ts +22 -0
  5. package/es/components/RouterBridge.d.ts +9 -0
  6. package/es/data-source/ExtendCollectionsProvider.d.ts +28 -2
  7. package/es/flow/FlowPage.d.ts +2 -1
  8. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  9. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  10. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  12. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  13. package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
  14. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  15. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  16. package/es/flow/components/FlowRoute.d.ts +10 -1
  17. package/es/flow/index.d.ts +4 -0
  18. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  19. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  20. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  21. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  22. package/es/index.d.ts +1 -0
  23. package/es/index.mjs +484 -437
  24. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  25. package/es/layout-manager/LayoutManager.d.ts +22 -0
  26. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  27. package/es/layout-manager/index.d.ts +13 -0
  28. package/es/layout-manager/types.d.ts +20 -0
  29. package/es/layout-manager/utils.d.ts +14 -0
  30. package/es/settings-center/index.d.ts +1 -1
  31. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  32. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  33. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  34. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  35. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  36. package/lib/index.js +484 -437
  37. package/package.json +8 -7
  38. package/src/Application.tsx +27 -12
  39. package/src/BaseApplication.tsx +6 -0
  40. package/src/PluginSettingsManager.ts +1 -1
  41. package/src/RouterManager.tsx +17 -1
  42. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  43. package/src/__tests__/app.test.tsx +8 -1
  44. package/src/__tests__/globalDeps.test.ts +1 -0
  45. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  46. package/src/__tests__/plugin-manager.test.tsx +177 -0
  47. package/src/__tests__/settings-center.test.tsx +24 -2
  48. package/src/components/KeepAlive.tsx +131 -0
  49. package/src/components/RouterBridge.tsx +28 -4
  50. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  51. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  52. package/src/data-source/ExtendCollectionsProvider.tsx +94 -20
  53. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  54. package/src/flow/FlowPage.tsx +35 -7
  55. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  56. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  57. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  58. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  59. package/src/flow/actions/aclCheck.tsx +4 -0
  60. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  61. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  62. package/src/flow/actions/linkageRules.tsx +122 -0
  63. package/src/flow/actions/openView.tsx +28 -4
  64. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  65. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  66. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  67. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
  69. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  70. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  71. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  72. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  73. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  74. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  75. package/src/flow/admin-shell/admin-layout/index.ts +2 -0
  76. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  77. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  78. package/src/flow/components/AdminLayout.tsx +4 -154
  79. package/src/flow/components/FlowRoute.tsx +105 -15
  80. package/src/flow/index.ts +4 -0
  81. package/src/flow/models/base/ActionModel.tsx +8 -1
  82. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  83. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  84. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  85. package/src/flow/models/base/RouteModel.tsx +1 -1
  86. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  87. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  88. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  89. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  90. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  91. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  92. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  93. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  94. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  95. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  96. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  97. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  98. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  99. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  100. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  101. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  102. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  103. package/src/index.ts +1 -0
  104. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  105. package/src/layout-manager/LayoutManager.tsx +185 -0
  106. package/src/layout-manager/LayoutRoute.tsx +138 -0
  107. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  108. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  109. package/src/layout-manager/index.ts +14 -0
  110. package/src/layout-manager/types.ts +22 -0
  111. package/src/layout-manager/utils.ts +37 -0
  112. package/src/nocobase-buildin-plugin/index.tsx +56 -48
  113. package/src/settings-center/index.ts +1 -1
  114. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  115. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  116. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  117. package/src/settings-center/plugin-manager/index.tsx +254 -0
  118. package/src/settings-center/plugin-manager/types.ts +35 -0
  119. package/src/settings-center/utils.tsx +8 -1
  120. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  121. package/src/theme/globalStyles.ts +10 -0
  122. package/src/utils/globalDeps.ts +2 -0
  123. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -0,0 +1,160 @@
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 { parsePathnameToViewParams, useFlowEngine } from '@nocobase/flow-engine';
11
+ import React, { type FC, useEffect, useMemo, useRef, useState } from 'react';
12
+ import { useLocation, useMatches, useNavigate } from 'react-router-dom';
13
+ import { NocoBaseDesktopRouteType } from '../../../flow-compat';
14
+ import { useApp } from '../../../hooks/useApp';
15
+ import { isLayoutContentRouteName } from '../../../layout-manager/utils';
16
+ import {
17
+ findFirstV2LandingRoute,
18
+ resolveAdminRouteRuntimeTarget,
19
+ toRouterNavigationPath,
20
+ } from './resolveAdminRouteRuntimeTarget';
21
+
22
+ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ children }) => {
23
+ const flowEngine = useFlowEngine();
24
+ const app = useApp();
25
+ const navigate = useNavigate();
26
+ const location = useLocation();
27
+ const matches = useMatches();
28
+ const [ready, setReady] = useState(false);
29
+ const replaceTriggeredRef = useRef(false);
30
+ const routeRepository = flowEngine.context.routeRepository;
31
+ const isAdminRoot = useMemo(() => {
32
+ const pathname = toRouterNavigationPath(location.pathname, app.router.getBasename());
33
+ return pathname === '/admin';
34
+ }, [app, location.pathname]);
35
+ const pageUid = useMemo(() => {
36
+ const lastMatch = matches[matches.length - 1];
37
+ if (!isLayoutContentRouteName('admin', lastMatch?.id)) {
38
+ return '';
39
+ }
40
+ const adminMatch = matches.find((match) => match.id === 'admin');
41
+ return (
42
+ (lastMatch.params?.name as string) ||
43
+ parsePathnameToViewParams(location.pathname, {
44
+ basePath: adminMatch?.pathname || '/admin',
45
+ })[0]?.viewUid ||
46
+ ''
47
+ );
48
+ }, [location.pathname, matches]);
49
+
50
+ useEffect(() => {
51
+ replaceTriggeredRef.current = false;
52
+ }, [location.pathname, location.search, location.hash, pageUid]);
53
+
54
+ useEffect(() => {
55
+ let active = true;
56
+
57
+ const run = async () => {
58
+ setReady(false);
59
+
60
+ if (!isAdminRoot && !pageUid) {
61
+ if (active) {
62
+ setReady(true);
63
+ }
64
+ return;
65
+ }
66
+
67
+ if (!routeRepository?.isAccessibleLoaded?.()) {
68
+ try {
69
+ await routeRepository?.ensureAccessibleLoaded?.();
70
+ } catch (_error) {
71
+ if (active) {
72
+ setReady(true);
73
+ }
74
+ return;
75
+ }
76
+ }
77
+
78
+ if (!active || replaceTriggeredRef.current) {
79
+ return;
80
+ }
81
+
82
+ if (pageUid) {
83
+ const currentRoute = routeRepository?.getRouteBySchemaUid?.(pageUid);
84
+ if (currentRoute?.type === NocoBaseDesktopRouteType.page) {
85
+ const target = resolveAdminRouteRuntimeTarget({
86
+ app,
87
+ route: currentRoute,
88
+ location: {
89
+ pathname: window.location.pathname,
90
+ search: window.location.search,
91
+ hash: window.location.hash,
92
+ },
93
+ preserveLocationState: true,
94
+ });
95
+
96
+ if (target.navigationMode === 'document' && target.runtimePath) {
97
+ replaceTriggeredRef.current = true;
98
+ window.location.replace(target.runtimePath);
99
+ return;
100
+ }
101
+ }
102
+
103
+ if (active) {
104
+ setReady(true);
105
+ }
106
+ return;
107
+ }
108
+
109
+ const firstAccessibleRoute = findFirstV2LandingRoute(routeRepository?.listAccessible?.() || []);
110
+ if (!firstAccessibleRoute) {
111
+ if (active) {
112
+ setReady(true);
113
+ }
114
+ return;
115
+ }
116
+
117
+ const target = resolveAdminRouteRuntimeTarget({
118
+ app,
119
+ route: firstAccessibleRoute,
120
+ });
121
+
122
+ if (!target.runtimePath) {
123
+ if (active) {
124
+ setReady(true);
125
+ }
126
+ return;
127
+ }
128
+
129
+ replaceTriggeredRef.current = true;
130
+ if (target.navigationMode === 'document') {
131
+ window.location.replace(target.runtimePath);
132
+ return;
133
+ }
134
+
135
+ navigate(toRouterNavigationPath(target.runtimePath, app.router.getBasename()), { replace: true });
136
+ };
137
+
138
+ void run();
139
+
140
+ return () => {
141
+ active = false;
142
+ };
143
+ }, [
144
+ app,
145
+ flowEngine,
146
+ isAdminRoot,
147
+ location.hash,
148
+ location.pathname,
149
+ location.search,
150
+ navigate,
151
+ pageUid,
152
+ routeRepository,
153
+ ]);
154
+
155
+ if (!ready) {
156
+ return null;
157
+ }
158
+
159
+ return <>{children}</>;
160
+ };
@@ -747,18 +747,6 @@ AdminLayoutMenuItemModel.registerFlow({
747
747
  });
748
748
  },
749
749
  },
750
- hidden: {
751
- title: 'Hidden',
752
- uiMode: { type: 'switch', key: 'hideInMenu' },
753
- defaultParams: async (ctx: FlowSettingsContext<AdminLayoutMenuItemModel>) => ({
754
- hideInMenu: !!ctx.model.getRoute()?.hideInMenu,
755
- }),
756
- beforeParamsSave: async (ctx: FlowSettingsContext<AdminLayoutMenuItemModel>, params) => {
757
- await ctx.model.updateMenuRoute({
758
- hideInMenu: !!params.hideInMenu,
759
- });
760
- },
761
- },
762
750
  linkageRules: {
763
751
  use: 'menuLinkageRules',
764
752
  hideInSettings: async (ctx: FlowSettingsContext<AdminLayoutMenuItemModel>) => ctx.model.isCreationSession(),
@@ -7,10 +7,17 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { define, observable, reaction } from '@formily/reactive';
10
+ import { define, observable } from '@formily/reactive';
11
11
  import { type FlowEngine, FlowModel } from '@nocobase/flow-engine';
12
+ import React from 'react';
12
13
  import type { NocoBaseDesktopRoute } from '../../../flow-compat';
13
- import { AdminLayoutRouteCoordinator, type RoutePageMeta } from '../AdminLayoutRouteCoordinator';
14
+ import { AdminLayoutRouteCoordinator } from '../AdminLayoutRouteCoordinator';
15
+ import {
16
+ BaseLayoutModel,
17
+ getLayoutModel,
18
+ type BaseLayoutStructure,
19
+ type GetLayoutModelOptions,
20
+ } from '../BaseLayoutModel';
14
21
  import { ADMIN_LAYOUT_MODEL_UID } from './constants';
15
22
  import { type AdminLayoutMenuItemModel } from './AdminLayoutMenuModels';
16
23
  import {
@@ -19,22 +26,17 @@ import {
19
26
  type AdminLayoutMenuRouteOptions,
20
27
  } from './AdminLayoutMenuUtils';
21
28
  import { AdminLayoutComponent } from './AdminLayoutComponent';
22
- import React from 'react';
23
29
  import { TopbarActionModel } from '../../models/topbar/TopbarActionModel';
24
30
  import { TopbarActionsBar } from './TopbarActionsBar';
31
+ import { AdminLayoutEntryGuard } from './AdminLayoutEntryGuard';
25
32
 
26
- export type AdminLayoutStructure = {
33
+ export type AdminLayoutStructure = BaseLayoutStructure & {
27
34
  subModels: {
28
35
  menuItems?: AdminLayoutMenuItemModel[];
29
36
  };
30
37
  };
31
38
 
32
- type GetAdminLayoutModelOptions<TModel extends FlowModel = AdminLayoutModel> = {
33
- required?: boolean;
34
- create?: boolean;
35
- props?: any;
36
- use?: new (...args: any[]) => TModel;
37
- };
39
+ type GetAdminLayoutModelOptions<TModel extends FlowModel = AdminLayoutModel> = GetLayoutModelOptions<TModel>;
38
40
 
39
41
  /**
40
42
  * Admin Layout 的纯运行时 host model。
@@ -48,19 +50,12 @@ type GetAdminLayoutModelOptions<TModel extends FlowModel = AdminLayoutModel> = {
48
50
  * model.syncMenuRoutes(routes);
49
51
  * ```
50
52
  */
51
- export class AdminLayoutModel extends FlowModel<AdminLayoutStructure> {
52
- isMobileLayout = false;
53
+ export class AdminLayoutModel extends BaseLayoutModel<AdminLayoutStructure> {
53
54
  menuRouteRefreshVersion = 0;
54
- private routeCoordinator?: AdminLayoutRouteCoordinator;
55
- private routeDisposer?: () => void;
56
- private activePageUid = '';
57
- private layoutContentElement: HTMLElement | null = null;
58
- private readonly routePageMetaMap = new Map<string, RoutePageMeta>();
59
55
 
60
56
  constructor(options: any) {
61
57
  super(options);
62
58
  define(this, {
63
- isMobileLayout: observable.ref,
64
59
  menuRouteRefreshVersion: observable.ref,
65
60
  });
66
61
  }
@@ -74,50 +69,6 @@ export class AdminLayoutModel extends FlowModel<AdminLayoutStructure> {
74
69
  this.menuRouteRefreshVersion += 1;
75
70
  }
76
71
 
77
- /**
78
- * 注册页面运行时信息。
79
- *
80
- * @param {string} pageUid 页面 UID
81
- * @param {RoutePageMeta} meta 页面运行时元数据
82
- * @returns {FlowModel} 对应的页面模型
83
- */
84
- registerRoutePage(pageUid: string, meta: RoutePageMeta) {
85
- this.routePageMetaMap.set(pageUid, meta);
86
- return this.getCoordinator().registerPage(pageUid, meta);
87
- }
88
-
89
- /**
90
- * 更新页面运行时信息。
91
- *
92
- * @param {string} pageUid 页面 UID
93
- * @param {Partial<RoutePageMeta>} meta 待更新的页面元数据
94
- * @returns {void}
95
- */
96
- updateRoutePage(pageUid: string, meta: Partial<RoutePageMeta>) {
97
- const prev = this.routePageMetaMap.get(pageUid) || { active: false };
98
- const next = {
99
- ...prev,
100
- ...meta,
101
- active: typeof meta.active === 'boolean' ? meta.active : prev.active,
102
- };
103
- this.routePageMetaMap.set(pageUid, next);
104
- this.getCoordinator().syncPageMeta(pageUid, meta);
105
- }
106
-
107
- /**
108
- * 注销页面运行时信息。
109
- *
110
- * @param {string} pageUid 页面 UID
111
- * @returns {void}
112
- */
113
- unregisterRoutePage(pageUid: string) {
114
- this.routePageMetaMap.delete(pageUid);
115
- if (this.activePageUid === pageUid) {
116
- this.activePageUid = '';
117
- }
118
- this.getCoordinator().unregisterPage(pageUid);
119
- }
120
-
121
72
  /**
122
73
  * 使用当前可访问菜单路由刷新 Layout 菜单树。
123
74
  *
@@ -157,131 +108,16 @@ export class AdminLayoutModel extends FlowModel<AdminLayoutStructure> {
157
108
  };
158
109
  }
159
110
 
160
- /**
161
- * 设置布局内容容器元素。
162
- *
163
- * @param {HTMLElement | null} element 布局内容容器
164
- * @returns {void}
165
- */
166
- setLayoutContentElement(element: HTMLElement | null) {
167
- this.layoutContentElement = element;
168
- this.getCoordinator().setLayoutContentElement(element);
169
- }
170
-
171
- /**
172
- * 设置是否为移动端布局。
173
- *
174
- * @param {boolean} isMobileLayout 是否为移动端布局
175
- * @returns {void}
176
- */
177
- setIsMobileLayout(isMobileLayout: boolean) {
178
- this.isMobileLayout = !!isMobileLayout;
179
- }
180
-
181
- protected onMount(): void {
182
- super.onMount();
183
- this.setupContextBindings();
184
- this.setupRouteReaction();
185
- }
186
-
187
- protected onUnmount(): void {
188
- this.teardownRuntime();
189
- super.onUnmount();
190
- }
191
-
192
- /**
193
- * 安装运行时上下文属性。
194
- *
195
- * @returns {void}
196
- */
197
- private setupContextBindings() {
198
- this.flowEngine.context.defineProperty('currentRoute', {
199
- get: () => this.getCurrentRouteByActivePage(),
200
- // 切页后需要立即读取当前激活页面的路由,不能复用首次访问时的缓存值。
201
- cache: false,
202
- });
203
- this.flowEngine.context.defineProperty('layoutContentElement', {
204
- get: () => this.layoutContentElement,
205
- // 布局容器 ref 会在挂载和卸载时变化,这里必须实时读取。
206
- cache: false,
207
- });
208
- this.flowEngine.context.defineProperty('isMobileLayout', {
209
- get: () => this.isMobileLayout,
210
- observable: true,
211
- cache: false,
212
- });
213
- }
214
-
215
- /**
216
- * 安装路由同步 reaction。
217
- *
218
- * @returns {void}
219
- */
220
- private setupRouteReaction() {
221
- if (this.routeDisposer) {
222
- return;
223
- }
224
-
225
- this.routeDisposer = reaction(
226
- () => this.flowEngine.context.route,
227
- (route) => {
228
- this.activePageUid = route?.params?.name || '';
229
- this.getCoordinator().syncRoute(route || {});
230
- },
231
- {
232
- fireImmediately: true,
233
- },
234
- );
235
- }
236
-
237
- /**
238
- * 释放运行时状态。
239
- *
240
- * @returns {void}
241
- */
242
- private teardownRuntime() {
243
- this.routeDisposer?.();
244
- this.routeDisposer = undefined;
245
- this.routeCoordinator?.destroy();
246
- this.routeCoordinator = undefined;
247
- this.routePageMetaMap.clear();
248
- this.activePageUid = '';
249
- this.layoutContentElement = null;
250
- }
251
-
252
- /**
253
- * 获取当前激活页面对应的路由对象。
254
- *
255
- * @returns {any} 当前激活页面对应的路由对象
256
- */
257
- private getCurrentRouteByActivePage() {
258
- return this.getCurrentRouteByPageUid(this.activePageUid);
259
- }
260
-
261
- /**
262
- * 根据页面 UID 获取路由对象。
263
- *
264
- * @param {string} pageUid 页面 UID
265
- * @returns {any} 路由对象
266
- */
267
- private getCurrentRouteByPageUid(pageUid: string) {
268
- return this.flowEngine.context.routeRepository?.getRouteBySchemaUid?.(pageUid) || {};
269
- }
270
-
271
- /**
272
- * 懒加载页面路由协调器。
273
- *
274
- * @returns {AdminLayoutRouteCoordinator} 路由协调器实例
275
- */
276
- private getCoordinator() {
277
- if (!this.routeCoordinator) {
278
- this.routeCoordinator = new AdminLayoutRouteCoordinator(this.flowEngine);
279
- }
280
- return this.routeCoordinator;
111
+ protected createRouteCoordinator() {
112
+ return new AdminLayoutRouteCoordinator(this.flowEngine, this.getRouteCoordinatorOptions());
281
113
  }
282
114
 
283
115
  render() {
284
- return <AdminLayoutComponent {...this.props} />;
116
+ return (
117
+ <AdminLayoutEntryGuard>
118
+ <AdminLayoutComponent {...this.props} model={this} />
119
+ </AdminLayoutEntryGuard>
120
+ );
285
121
  }
286
122
  }
287
123
 
@@ -292,10 +128,12 @@ AdminLayoutModel.registerFlow({
292
128
  handler: async (ctx, params) => {
293
129
  const topbarActionModels = await ctx.engine.getSubclassesOfAsync('TopbarActionModel');
294
130
  const actions = [...topbarActionModels.keys()].map<TopbarActionModel>((name) => {
295
- return ctx.engine.createModel({
131
+ const action = ctx.engine.createModel<TopbarActionModel>({
296
132
  use: name,
297
133
  uid: `topbar-action-${name}`,
298
134
  });
135
+ action.setParent(ctx.model);
136
+ return action;
299
137
  });
300
138
  ctx.model.props.actionsRender = (props) => {
301
139
  return [<TopbarActionsBar key="topbar-actions" actions={actions} isMobile={props?.isMobile} />];
@@ -317,23 +155,12 @@ export function getAdminLayoutModel<TModel extends FlowModel = AdminLayoutModel>
317
155
  flowEngine: FlowEngine,
318
156
  options: GetAdminLayoutModelOptions<TModel> = {},
319
157
  ) {
320
- const { required = false, create = false, props, use } = options;
321
- let model = flowEngine.getModel<TModel>(ADMIN_LAYOUT_MODEL_UID);
322
-
323
- if (!model && create) {
324
- const ModelClass = (use || AdminLayoutModel) as new (...args: any[]) => TModel;
325
- model = flowEngine.createModel<TModel>({
326
- uid: ADMIN_LAYOUT_MODEL_UID,
327
- use: ModelClass,
328
- props,
329
- });
330
- }
331
-
332
- if (model && props) {
333
- model.setProps(props);
334
- }
158
+ const model = getLayoutModel<TModel>(flowEngine, ADMIN_LAYOUT_MODEL_UID, {
159
+ ...options,
160
+ use: (options.use || AdminLayoutModel) as any,
161
+ });
335
162
 
336
- if (!model && required) {
163
+ if (!model && options.required) {
337
164
  throw new Error('[NocoBase] FlowRoute requires admin-layout-model. Please render FlowRoute under AdminLayout.');
338
165
  }
339
166
 
@@ -13,7 +13,9 @@ import { observer, useFlowEngine } from '@nocobase/flow-engine';
13
13
  import { Result, theme as antdTheme } from 'antd';
14
14
  import React, { FC, useCallback, useMemo } from 'react';
15
15
  import { useTranslation } from 'react-i18next';
16
- import { Outlet, useLocation } from 'react-router-dom';
16
+ import { Outlet, useLocation, useMatches, useParams } from 'react-router-dom';
17
+ import { KeepAlive } from '../../../components/KeepAlive';
18
+ import { getLayoutContentRouteNames } from '../../../layout-manager/utils';
17
19
  import { isV2AdminRuntime, isV2MenuRoute } from './resolveAdminRouteRuntimeTarget';
18
20
 
19
21
  type AdminLayoutContentProps = {
@@ -47,6 +49,8 @@ const mobileHeight = {
47
49
  height: `calc(100dvh - var(--nb-header-height))`,
48
50
  };
49
51
 
52
+ const adminLayoutContentRouteNames = getLayoutContentRouteNames('admin');
53
+
50
54
  /**
51
55
  * 检测当前浏览器是否支持 dvh,移动端支持时优先使用它计算可视区域高度。
52
56
  *
@@ -93,6 +97,11 @@ const ShowTipWhenNoPages = observer(() => {
93
97
  */
94
98
  export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ onContentElementChange }) => {
95
99
  const style = useMemo(() => (isDvhSupported() ? mobileHeight : undefined), []);
100
+ const params = useParams();
101
+ const matches = useMatches();
102
+ const pageUid = params.name;
103
+ const currentRouteId = matches.at(-1)?.id;
104
+ const shouldKeepAlive = !!pageUid && adminLayoutContentRouteNames.includes(currentRouteId || '');
96
105
  const bindLayoutContentRef = useCallback(
97
106
  (node: HTMLDivElement | null) => {
98
107
  // shell 直接渲染内容区时,仍需把挂载目标同步给 root model。
@@ -108,7 +117,7 @@ export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ onContentEleme
108
117
  style={style}
109
118
  >
110
119
  <div style={pageContentStyle}>
111
- <Outlet />
120
+ {shouldKeepAlive && pageUid ? <KeepAlive uid={pageUid}>{() => <Outlet />}</KeepAlive> : <Outlet />}
112
121
  <ShowTipWhenNoPages />
113
122
  </div>
114
123
  </div>
@@ -891,34 +891,9 @@ describe('AdminLayoutModel menu items', () => {
891
891
  });
892
892
  });
893
893
 
894
- it('should persist hidden setting through route repository', async () => {
895
- const updateRoute = vi.fn().mockResolvedValue(undefined);
896
- engine.context.routeRepository.updateRoute = updateRoute;
897
-
898
- const model = engine.createModel<AdminLayoutMenuItemModel>({
899
- uid: 'menu-item-hidden',
900
- use: AdminLayoutMenuItemModel,
901
- props: {
902
- route: {
903
- id: 1,
904
- title: 'Page 1',
905
- schemaUid: 'current-page',
906
- type: NocoBaseDesktopRouteType.page,
907
- hideInMenu: false,
908
- },
909
- },
910
- });
911
-
912
- const menuSettingsFlow = AdminLayoutMenuItemModel.globalFlowRegistry.getFlow('menuSettings');
913
- await menuSettingsFlow?.steps?.hidden?.beforeParamsSave?.({ model } as any, { hideInMenu: true }, {});
914
-
915
- expect(updateRoute).toHaveBeenCalledWith(1, {
916
- hideInMenu: true,
917
- });
918
- });
919
-
920
894
  it('should expose menu linkage rules only for existing menu items', async () => {
921
895
  const menuSettingsFlow = AdminLayoutMenuItemModel.globalFlowRegistry.getFlow('menuSettings');
896
+ expect(menuSettingsFlow?.steps?.hidden).toBeUndefined();
922
897
  expect(menuSettingsFlow?.steps?.linkageRules?.use).toBe('menuLinkageRules');
923
898
 
924
899
  const model = engine.createModel<AdminLayoutMenuItemModel>({