@nocobase/client-v2 2.1.31 → 2.2.0-alpha.10

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 (170) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +2 -0
  3. package/es/PluginSettingsManager.d.ts +33 -0
  4. package/es/RouteRepository.d.ts +29 -6
  5. package/es/RouterManager.d.ts +15 -0
  6. package/es/authRedirect.d.ts +1 -0
  7. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  8. package/es/components/category-tabs/index.d.ts +9 -0
  9. package/es/components/form/TypedVariableInput.d.ts +31 -5
  10. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  11. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  12. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  13. package/es/components/form/filter/index.d.ts +2 -0
  14. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  15. package/es/components/index.d.ts +2 -0
  16. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  17. package/es/entry-actions/index.d.ts +9 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +2 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  22. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
  23. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  25. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  26. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  27. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  28. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  29. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  30. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  31. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  34. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  35. package/es/flow/index.d.ts +1 -0
  36. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  37. package/es/flow/models/base/GridModel.d.ts +1 -1
  38. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  39. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  40. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  41. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
  42. package/es/flow-compat/routeTypes.d.ts +1 -0
  43. package/es/index.d.ts +7 -1
  44. package/es/index.mjs +223 -135
  45. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  46. package/es/utils/index.d.ts +1 -0
  47. package/lib/index.js +220 -132
  48. package/package.json +7 -7
  49. package/src/APIClient.ts +1 -10
  50. package/src/Application.tsx +4 -0
  51. package/src/BaseApplication.tsx +2 -0
  52. package/src/PluginSettingsManager.ts +53 -0
  53. package/src/RouteRepository.ts +151 -24
  54. package/src/RouterManager.tsx +142 -1
  55. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  56. package/src/__tests__/RouteRepository.test.ts +239 -0
  57. package/src/__tests__/RouterManager.test.ts +125 -0
  58. package/src/__tests__/app.test.tsx +18 -0
  59. package/src/__tests__/authRedirect.test.ts +17 -0
  60. package/src/__tests__/browserChecker.test.ts +61 -0
  61. package/src/__tests__/exports.test.ts +16 -0
  62. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  63. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
  64. package/src/__tests__/plugin-manager.test.tsx +44 -2
  65. package/src/__tests__/settings-center.test.tsx +40 -1
  66. package/src/authRedirect.ts +38 -0
  67. package/src/components/README.md +7 -1
  68. package/src/components/README.zh-CN.md +6 -1
  69. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  70. package/src/components/category-tabs/index.ts +10 -0
  71. package/src/components/form/JsonTextArea.tsx +4 -0
  72. package/src/components/form/TypedVariableInput.tsx +452 -101
  73. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  74. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  75. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  76. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  77. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  78. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  81. package/src/components/index.ts +2 -0
  82. package/src/entry-actions/EntryActionManager.ts +76 -0
  83. package/src/entry-actions/index.ts +10 -0
  84. package/src/flow/__tests__/FlowRoute.test.tsx +1084 -145
  85. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  86. package/src/flow/actions/customVariable.tsx +1 -2
  87. package/src/flow/actions/index.ts +2 -0
  88. package/src/flow/actions/linkageRules.tsx +65 -8
  89. package/src/flow/actions/runjs.tsx +2 -14
  90. package/src/flow/admin-shell/BaseLayoutModel.tsx +13 -0
  91. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +46 -13
  92. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  93. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  94. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  95. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  96. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  97. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  98. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  99. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  100. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  101. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
  102. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  103. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +77 -0
  104. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  105. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  106. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  107. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  108. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  109. package/src/flow/components/DefaultValue.tsx +1 -2
  110. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  111. package/src/flow/components/FieldAssignValueInput.tsx +76 -9
  112. package/src/flow/components/FlowRoute.tsx +185 -28
  113. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  114. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
  115. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  116. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  117. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  118. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  119. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  120. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  121. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  122. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  123. package/src/flow/index.ts +1 -0
  124. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  125. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  126. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  127. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  128. package/src/flow/models/base/ActionModel.tsx +21 -8
  129. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  130. package/src/flow/models/base/GridModel.tsx +38 -7
  131. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  132. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  133. package/src/flow/models/base/PageModel/PageTabModel.tsx +186 -3
  134. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -14
  135. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  136. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  137. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +586 -1
  138. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  139. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  140. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  141. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -2
  142. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  143. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  144. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
  145. package/src/flow/models/blocks/form/value-runtime/rules.ts +2 -2
  146. package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
  147. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  148. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  149. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  150. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  151. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  152. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  153. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  154. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  155. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  156. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  157. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  158. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  159. package/src/flow-compat/Popover.tsx +43 -4
  160. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  161. package/src/flow-compat/routeTypes.ts +1 -0
  162. package/src/index.ts +13 -1
  163. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  164. package/src/nocobase-buildin-plugin/index.tsx +19 -3
  165. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  166. package/src/settings-center/SystemSettingsPage.tsx +0 -1
  167. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  168. package/src/settings-center/utils.tsx +0 -6
  169. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  170. package/src/utils/index.tsx +1 -0
@@ -0,0 +1,435 @@
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 { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
11
+ import { act, render, screen, waitFor } from '@testing-library/react';
12
+ import React, { useEffect } from 'react';
13
+ import { createMemoryRouter, RouterProvider, useParams } from 'react-router-dom';
14
+ import { afterEach, describe, expect, it, vi } from 'vitest';
15
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../../flow-compat';
16
+ import type { LayoutDefinition } from '../../../../layout-manager/types';
17
+ import { AdminLayoutComponent } from '../AdminLayoutComponent';
18
+ import { AdminLayoutModel } from '../AdminLayoutModel';
19
+ import { AdminLayoutMenuItemModel } from '../AdminLayoutMenuModels';
20
+ import { AdminLayoutContent } from '../AdminLayoutSlotModels';
21
+
22
+ const proLayoutLifecycle = vi.hoisted(() => ({
23
+ events: [] as string[],
24
+ routes: [] as Array<{ children?: Array<{ name?: React.ReactNode }> }>,
25
+ }));
26
+
27
+ vi.mock('@ant-design/pro-layout', async () => {
28
+ const ReactModule = await import('react');
29
+ const RouteContext = ReactModule.createContext({ isMobile: false });
30
+ const ProLayoutMock = (props: {
31
+ children?: React.ReactNode;
32
+ route?: { children?: Array<{ name?: React.ReactNode }> };
33
+ }) => {
34
+ ReactModule.useEffect(() => {
35
+ proLayoutLifecycle.events.push('pro-layout:mount');
36
+ return () => {
37
+ proLayoutLifecycle.events.push('pro-layout:unmount');
38
+ };
39
+ }, []);
40
+ proLayoutLifecycle.routes.push(props.route || {});
41
+
42
+ return ReactModule.createElement(
43
+ RouteContext.Provider,
44
+ { value: { isMobile: false } },
45
+ ReactModule.createElement(
46
+ 'div',
47
+ { 'data-testid': 'pro-layout' },
48
+ ReactModule.createElement(
49
+ 'nav',
50
+ { 'data-testid': 'pro-layout-menu' },
51
+ (props.route?.children || []).map((item) =>
52
+ ReactModule.createElement(
53
+ 'span',
54
+ { key: String(item.name), 'data-testid': 'pro-layout-menu-item' },
55
+ item.name,
56
+ ),
57
+ ),
58
+ ),
59
+ props.children,
60
+ ),
61
+ );
62
+ };
63
+
64
+ return {
65
+ default: ProLayoutMock,
66
+ RouteContext,
67
+ };
68
+ });
69
+
70
+ vi.mock('../../../system-settings', () => ({
71
+ useSystemSettings: () => ({
72
+ loading: false,
73
+ data: {
74
+ data: {
75
+ title: 'NocoBase',
76
+ },
77
+ },
78
+ }),
79
+ }));
80
+
81
+ vi.mock('../useApplications', () => ({
82
+ useApplications: () => ({
83
+ appList: [],
84
+ }),
85
+ }));
86
+
87
+ vi.mock('../AppListRender', () => ({
88
+ useAppListRender: () => undefined,
89
+ }));
90
+
91
+ afterEach(() => {
92
+ delete (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__;
93
+ });
94
+
95
+ describe('AdminLayoutComponent', () => {
96
+ it('keeps custom admin layout pages alive by layout route name', async () => {
97
+ const layout: LayoutDefinition = {
98
+ routeName: 'admin2',
99
+ routePath: '/admin2',
100
+ rootRouteName: 'admin2',
101
+ uid: 'custom-admin-layout-model',
102
+ layoutModelClass: 'AdminLayoutModel',
103
+ rootPageModelClass: 'RootPageModel',
104
+ childPageModelClass: 'ChildPageModel',
105
+ authCheck: true,
106
+ };
107
+ const events: string[] = [];
108
+ const engine = new FlowEngine();
109
+ engine.context.defineProperty('routeRepository', {
110
+ value: {
111
+ listAccessible: () => [],
112
+ },
113
+ });
114
+
115
+ const Page = () => {
116
+ const { name } = useParams();
117
+ useEffect(() => {
118
+ events.push(`mount:${name}`);
119
+ return () => {
120
+ events.push(`unmount:${name}`);
121
+ };
122
+ }, [name]);
123
+
124
+ return <div data-testid={`admin-layout-page-${name}`}>page {name}</div>;
125
+ };
126
+
127
+ const router = createMemoryRouter(
128
+ [
129
+ {
130
+ id: layout.routeName,
131
+ path: layout.routePath,
132
+ element: <AdminLayoutContent layout={layout} />,
133
+ children: [
134
+ {
135
+ id: 'admin2.__page',
136
+ path: ':name',
137
+ element: <Page />,
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ {
143
+ initialEntries: ['/admin2/page-a'],
144
+ },
145
+ );
146
+
147
+ render(
148
+ <FlowEngineProvider engine={engine}>
149
+ <RouterProvider router={router} />
150
+ </FlowEngineProvider>,
151
+ );
152
+
153
+ expect(await screen.findByTestId('admin-layout-page-page-a')).toBeInTheDocument();
154
+
155
+ await act(async () => {
156
+ await router.navigate('/admin2/page-b');
157
+ });
158
+
159
+ expect(await screen.findByTestId('admin-layout-page-page-b')).toBeInTheDocument();
160
+ expect(screen.getByTestId('admin-layout-page-page-a')).toBeInTheDocument();
161
+ expect(events).toEqual(['mount:page-a', 'mount:page-b']);
162
+ });
163
+
164
+ it('activates the current layout before loading accessible routes', async () => {
165
+ const layout: LayoutDefinition = {
166
+ routeName: 'admin2',
167
+ routePath: '/admin2',
168
+ rootRouteName: 'admin2',
169
+ uid: 'custom-admin-layout-model',
170
+ layoutModelClass: 'AdminLayoutModel',
171
+ rootPageModelClass: 'RootPageModel',
172
+ childPageModelClass: 'ChildPageModel',
173
+ authCheck: true,
174
+ };
175
+ const deactivateLayout = vi.fn();
176
+ const activateLayout = vi.fn(() => deactivateLayout);
177
+ const ensureAccessibleLoaded = vi.fn(async () => []);
178
+ const engine = new FlowEngine();
179
+ engine.context.defineProperty('routeRepository', {
180
+ value: {
181
+ activateLayout,
182
+ ensureAccessibleLoaded,
183
+ listAccessible: () => [],
184
+ subscribe: vi.fn(),
185
+ unsubscribe: vi.fn(),
186
+ },
187
+ });
188
+ engine.context.defineProperty('t', {
189
+ value: (key: string) => key,
190
+ });
191
+ const model = engine.createModel<AdminLayoutModel>({
192
+ uid: layout.uid,
193
+ use: AdminLayoutModel,
194
+ props: {
195
+ layout,
196
+ },
197
+ });
198
+
199
+ const router = createMemoryRouter(
200
+ [
201
+ {
202
+ id: layout.routeName,
203
+ path: layout.routePath,
204
+ element: <AdminLayoutComponent model={model} />,
205
+ },
206
+ ],
207
+ {
208
+ initialEntries: ['/admin2'],
209
+ },
210
+ );
211
+ const { unmount } = render(
212
+ <FlowEngineProvider engine={engine}>
213
+ <RouterProvider router={router} />
214
+ </FlowEngineProvider>,
215
+ );
216
+
217
+ await waitFor(() => {
218
+ expect(ensureAccessibleLoaded).toHaveBeenCalled();
219
+ });
220
+ expect(activateLayout).toHaveBeenCalledWith(layout);
221
+ expect(activateLayout.mock.invocationCallOrder[0]).toBeLessThan(ensureAccessibleLoaded.mock.invocationCallOrder[0]);
222
+
223
+ unmount();
224
+
225
+ expect(deactivateLayout).toHaveBeenCalledTimes(1);
226
+ });
227
+
228
+ it('keeps layout content mounted when the menu route tree refreshes', async () => {
229
+ proLayoutLifecycle.events = [];
230
+ proLayoutLifecycle.routes = [];
231
+ const layout: LayoutDefinition = {
232
+ routeName: 'admin',
233
+ routePath: '/admin',
234
+ rootRouteName: 'admin',
235
+ uid: 'admin-layout-model',
236
+ layoutModelClass: 'AdminLayoutModel',
237
+ rootPageModelClass: 'RootPageModel',
238
+ childPageModelClass: 'ChildPageModel',
239
+ authCheck: true,
240
+ };
241
+ const events: string[] = [];
242
+ const engine = new FlowEngine();
243
+ engine.context.defineProperty('routeRepository', {
244
+ value: {
245
+ activateLayout: vi.fn(() => vi.fn()),
246
+ ensureAccessibleLoaded: vi.fn(async () => []),
247
+ listAccessible: () => [],
248
+ subscribe: vi.fn(),
249
+ unsubscribe: vi.fn(),
250
+ },
251
+ });
252
+ engine.context.defineProperty('t', {
253
+ value: (key: string) => key,
254
+ });
255
+ const model = engine.createModel<AdminLayoutModel>({
256
+ uid: layout.uid,
257
+ use: AdminLayoutModel,
258
+ props: {
259
+ layout,
260
+ },
261
+ });
262
+
263
+ const Page = () => {
264
+ useEffect(() => {
265
+ events.push('page:mount');
266
+ return () => {
267
+ events.push('page:unmount');
268
+ };
269
+ }, []);
270
+
271
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
272
+ };
273
+
274
+ const router = createMemoryRouter(
275
+ [
276
+ {
277
+ id: layout.routeName,
278
+ path: layout.routePath,
279
+ element: <AdminLayoutComponent model={model} />,
280
+ children: [
281
+ {
282
+ id: 'admin.workflow.tasks',
283
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
284
+ element: <Page />,
285
+ },
286
+ ],
287
+ },
288
+ ],
289
+ {
290
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
291
+ },
292
+ );
293
+
294
+ render(
295
+ <FlowEngineProvider engine={engine}>
296
+ <RouterProvider router={router} />
297
+ </FlowEngineProvider>,
298
+ );
299
+
300
+ expect(await screen.findByTestId('custom-admin-route')).toBeInTheDocument();
301
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
302
+
303
+ act(() => {
304
+ model.refreshMenuRouteTree();
305
+ });
306
+
307
+ await act(async () => {
308
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
309
+ });
310
+
311
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
312
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
313
+ expect(events).toEqual(['page:mount']);
314
+ });
315
+
316
+ it('updates ProLayout menu routes after accessible routes change without remounting the layout', async () => {
317
+ proLayoutLifecycle.events = [];
318
+ proLayoutLifecycle.routes = [];
319
+ (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__ = 'v2';
320
+ const layout: LayoutDefinition = {
321
+ routeName: 'admin',
322
+ routePath: '/admin',
323
+ rootRouteName: 'admin',
324
+ uid: 'admin-layout-model',
325
+ layoutModelClass: 'AdminLayoutModel',
326
+ rootPageModelClass: 'RootPageModel',
327
+ childPageModelClass: 'ChildPageModel',
328
+ authCheck: true,
329
+ };
330
+ let accessibleRoutes: NocoBaseDesktopRoute[] = [
331
+ {
332
+ id: 1,
333
+ title: 'Visible page',
334
+ schemaUid: 'visible-page',
335
+ type: NocoBaseDesktopRouteType.flowPage,
336
+ },
337
+ ];
338
+ let routeRepositorySubscriber: (() => void) | undefined;
339
+ const events: string[] = [];
340
+ const engine = new FlowEngine();
341
+ engine.registerModels({
342
+ AdminLayoutMenuItemModel,
343
+ });
344
+ engine.context.defineProperty('routeRepository', {
345
+ value: {
346
+ activateLayout: vi.fn(() => vi.fn()),
347
+ ensureAccessibleLoaded: vi.fn(async () => accessibleRoutes),
348
+ listAccessible: () => accessibleRoutes,
349
+ subscribe: vi.fn((subscriber: () => void) => {
350
+ routeRepositorySubscriber = subscriber;
351
+ }),
352
+ unsubscribe: vi.fn(),
353
+ },
354
+ });
355
+ engine.context.defineProperty('app', {
356
+ value: {
357
+ getPublicPath: () => '/v/',
358
+ router: {
359
+ getBasename: () => '/v',
360
+ },
361
+ },
362
+ });
363
+ engine.context.defineProperty('t', {
364
+ value: (key: string) => key,
365
+ });
366
+ const model = engine.createModel<AdminLayoutModel>({
367
+ uid: layout.uid,
368
+ use: AdminLayoutModel,
369
+ props: {
370
+ layout,
371
+ },
372
+ });
373
+
374
+ const Page = () => {
375
+ useEffect(() => {
376
+ events.push('page:mount');
377
+ return () => {
378
+ events.push('page:unmount');
379
+ };
380
+ }, []);
381
+
382
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
383
+ };
384
+
385
+ const router = createMemoryRouter(
386
+ [
387
+ {
388
+ id: layout.routeName,
389
+ path: layout.routePath,
390
+ element: <AdminLayoutComponent model={model} />,
391
+ children: [
392
+ {
393
+ id: 'admin.workflow.tasks',
394
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
395
+ element: <Page />,
396
+ },
397
+ ],
398
+ },
399
+ ],
400
+ {
401
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
402
+ },
403
+ );
404
+
405
+ render(
406
+ <FlowEngineProvider engine={engine}>
407
+ <RouterProvider router={router} />
408
+ </FlowEngineProvider>,
409
+ );
410
+
411
+ expect(await screen.findByText('Visible page')).toBeInTheDocument();
412
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
413
+
414
+ act(() => {
415
+ accessibleRoutes = [];
416
+ routeRepositorySubscriber?.();
417
+ });
418
+
419
+ await waitFor(() => {
420
+ expect(screen.queryByText('Visible page')).not.toBeInTheDocument();
421
+ });
422
+
423
+ await act(async () => {
424
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
425
+ });
426
+
427
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
428
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
429
+ expect(events).toEqual(['page:mount']);
430
+ expect(
431
+ proLayoutLifecycle.routes.some((route) => route.children?.some((item) => item.name === 'Visible page')),
432
+ ).toBe(true);
433
+ expect(proLayoutLifecycle.routes.at(-1)?.children?.some((item) => item.name === 'Visible page')).toBe(false);
434
+ });
435
+ });
@@ -318,6 +318,50 @@ describe('AdminLayoutModel menu items', () => {
318
318
  expect(route.children[1]._model).toBe(adminLayoutModel.subModels.menuItems?.[1]);
319
319
  });
320
320
 
321
+ it('should generate ProLayout route tree under a custom admin layout route path', () => {
322
+ const adminLayoutModel = engine.createModel<AdminLayoutModel>({
323
+ uid: 'admin2-layout-model',
324
+ use: AdminLayoutModel,
325
+ props: {
326
+ layout: {
327
+ routeName: 'admin2',
328
+ routePath: '/admin2',
329
+ uid: 'admin2-layout-model',
330
+ layoutModelClass: 'AdminLayoutModel',
331
+ },
332
+ },
333
+ });
334
+
335
+ adminLayoutModel.syncMenuRoutes([
336
+ {
337
+ id: 1,
338
+ title: 'Group',
339
+ type: NocoBaseDesktopRouteType.group,
340
+ children: [
341
+ {
342
+ id: 11,
343
+ title: 'Page 1',
344
+ schemaUid: 'page-1',
345
+ type: NocoBaseDesktopRouteType.flowPage,
346
+ },
347
+ ],
348
+ },
349
+ ]);
350
+
351
+ const route = adminLayoutModel.toProLayoutRoute({
352
+ designable: false,
353
+ isMobile: false,
354
+ t: (title) => title,
355
+ });
356
+
357
+ expect(route.children[0].path).toBe('/admin2/1');
358
+ expect(route.children[0].redirect).toBe('/admin2/page-1');
359
+ expect(route.children[0]._runtimePath).toBe('/apps/demo/v2/admin2/page-1');
360
+ expect(route.children[0].routes?.[0].path).toBe('/admin2/page-1');
361
+ expect(route.children[0].routes?.[0].redirect).toBe('/admin2/page-1');
362
+ expect(route.children[0].routes?.[0]._runtimePath).toBe('/apps/demo/v2/admin2/page-1');
363
+ });
364
+
321
365
  it('should filter legacy page menu routes but keep empty groups in v2 admin layout', () => {
322
366
  const adminLayoutModel = engine.createModel<AdminLayoutModel>({
323
367
  uid: 'admin-layout-model',
@@ -1128,6 +1172,46 @@ describe('AdminLayoutModel menu items', () => {
1128
1172
  expect(adminLayoutModel.menuRouteRefreshVersion).toBe(refreshBefore + 1);
1129
1173
  });
1130
1174
 
1175
+ it('should refresh the owning admin layout when hiding custom layout menu routes dynamically', () => {
1176
+ const defaultAdminLayoutModel = engine.createModel<AdminLayoutModel>({
1177
+ uid: ADMIN_LAYOUT_MODEL_UID,
1178
+ use: AdminLayoutModel,
1179
+ });
1180
+ const customAdminLayoutModel = engine.createModel<AdminLayoutModel>({
1181
+ uid: 'custom-admin-layout-model',
1182
+ use: AdminLayoutModel,
1183
+ props: {
1184
+ layout: {
1185
+ routeName: 'admin2',
1186
+ routePath: '/admin2',
1187
+ rootRouteName: 'admin2',
1188
+ uid: 'custom-admin-layout-model',
1189
+ layoutModelClass: 'AdminLayoutModel',
1190
+ rootPageModelClass: 'RootPageModel',
1191
+ childPageModelClass: 'ChildPageModel',
1192
+ authCheck: true,
1193
+ },
1194
+ },
1195
+ });
1196
+ const model = engine.createModel<AdminLayoutMenuItemModel>({
1197
+ uid: 'menu-item-custom-layout-dynamic-hidden',
1198
+ use: AdminLayoutMenuItemModel,
1199
+ props: {
1200
+ route: createRoute(),
1201
+ },
1202
+ });
1203
+
1204
+ model.setParent(customAdminLayoutModel);
1205
+
1206
+ const defaultRefreshBefore = defaultAdminLayoutModel.menuRouteRefreshVersion;
1207
+ const customRefreshBefore = customAdminLayoutModel.menuRouteRefreshVersion;
1208
+
1209
+ model.setHidden(true);
1210
+
1211
+ expect(customAdminLayoutModel.menuRouteRefreshVersion).toBe(customRefreshBefore + 1);
1212
+ expect(defaultAdminLayoutModel.menuRouteRefreshVersion).toBe(defaultRefreshBefore);
1213
+ });
1214
+
1131
1215
  it('should render hidden menu item with opacity and keep original title in config mode', () => {
1132
1216
  const model = engine.createModel<AdminLayoutMenuItemModel>({
1133
1217
  uid: 'menu-item-hidden-in-config',
@@ -2222,6 +2306,21 @@ describe('AdminLayoutModel menu items', () => {
2222
2306
  );
2223
2307
  });
2224
2308
 
2309
+ it('should include router basename when opening same-origin links in a new window', async () => {
2310
+ await openAdminLayoutMenuLink({
2311
+ context: engine.context as any,
2312
+ href: '/admin2/page',
2313
+ params: [{ name: 'from', value: 'admin' }],
2314
+ openInNewWindow: true,
2315
+ isMobile: false,
2316
+ closeMobileMenu: vi.fn(),
2317
+ navigate: navigateMock,
2318
+ basenameOfCurrentRouter: '/v',
2319
+ });
2320
+
2321
+ expect(window.open).toHaveBeenCalledWith('/v/admin2/page?from=admin', '_blank', 'noopener,noreferrer');
2322
+ });
2323
+
2225
2324
  it('should resolve sibling move options for non-group drag target', () => {
2226
2325
  const activeModel = engine.createModel<AdminLayoutMenuItemModel>({
2227
2326
  uid: 'drag-source-page',
@@ -92,6 +92,7 @@ import {
92
92
  type FlowModel,
93
93
  } from '@nocobase/flow-engine';
94
94
  import { AdminLayoutComponent, AdminLayoutModel, getAdminLayoutModel } from '..';
95
+ import { NocoBaseDesktopRouteType } from '../../../../flow-compat';
95
96
  import { getLayoutPageRouteName, getLayoutPageViewRouteName } from '../../../../layout-manager/utils';
96
97
  import { TopbarActionModel } from '../../../models/topbar/TopbarActionModel';
97
98
  import { UserCenterTopbarActionModel } from '../../../models/topbar/UserCenterTopbarActionModel';
@@ -432,6 +433,82 @@ describe('AdminLayoutModel runtime', () => {
432
433
  });
433
434
  });
434
435
 
436
+ it('should resolve group id paths as blank root routes', async () => {
437
+ const engine = new FlowEngine();
438
+ engine.context.defineProperty('routeRepository', {
439
+ value: {
440
+ getRouteById: (routeId: string) =>
441
+ routeId === '1'
442
+ ? {
443
+ id: 1,
444
+ type: NocoBaseDesktopRouteType.group,
445
+ }
446
+ : undefined,
447
+ },
448
+ });
449
+
450
+ render(
451
+ <FlowEngineProvider engine={engine}>
452
+ <TestAdminLayoutHost />
453
+ </FlowEngineProvider>,
454
+ );
455
+ const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
456
+ expect(model).toBeTruthy();
457
+
458
+ expect(
459
+ model.resolveLayoutRoute({
460
+ name: getLayoutPageRouteName('admin'),
461
+ pathname: '/admin/1',
462
+ layoutBasePathname: '/admin',
463
+ }),
464
+ ).toMatchObject({
465
+ type: 'root',
466
+ pathname: '/admin/1',
467
+ relativePath: '1',
468
+ });
469
+ });
470
+
471
+ it('should prefer schema uid routes before treating numeric paths as group ids', async () => {
472
+ const engine = new FlowEngine();
473
+ engine.context.defineProperty('routeRepository', {
474
+ value: {
475
+ getRouteBySchemaUid: (pageUid: string) =>
476
+ pageUid === '1'
477
+ ? {
478
+ schemaUid: '1',
479
+ type: NocoBaseDesktopRouteType.flowPage,
480
+ }
481
+ : undefined,
482
+ getRouteById: (routeId: string) =>
483
+ routeId === '1'
484
+ ? {
485
+ id: 1,
486
+ type: NocoBaseDesktopRouteType.group,
487
+ }
488
+ : undefined,
489
+ },
490
+ });
491
+
492
+ render(
493
+ <FlowEngineProvider engine={engine}>
494
+ <TestAdminLayoutHost />
495
+ </FlowEngineProvider>,
496
+ );
497
+ const model = engine.getModel<TestAdminLayoutModel>('admin-layout-model');
498
+ expect(model).toBeTruthy();
499
+
500
+ expect(
501
+ model.resolveLayoutRoute({
502
+ name: getLayoutPageRouteName('admin'),
503
+ pathname: '/admin/1',
504
+ layoutBasePathname: '/admin',
505
+ }),
506
+ ).toMatchObject({
507
+ type: 'page',
508
+ pageUid: '1',
509
+ });
510
+ });
511
+
435
512
  it('should not consume global routes that belong to nested layouts', async () => {
436
513
  const engine = new FlowEngine();
437
514
  const routeRef = observable.ref({