@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (220) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/JsonTextArea.d.ts +2 -1
  6. package/es/components/form/TypedVariableInput.d.ts +22 -4
  7. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  9. package/es/components/form/filter/index.d.ts +2 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/actions/afterSuccess.d.ts +8 -1
  13. package/es/flow/actions/index.d.ts +1 -1
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  16. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  19. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  20. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  21. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  22. package/es/flow/index.d.ts +1 -0
  23. package/es/flow/models/base/GridModel.d.ts +1 -1
  24. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  25. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  26. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  27. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  28. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  33. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  35. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  36. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  37. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  38. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  39. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  40. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  41. package/es/flow/utils/formValueDeps.d.ts +32 -0
  42. package/es/flow-compat/FieldValidation.d.ts +2 -1
  43. package/es/index.d.ts +2 -1
  44. package/es/index.mjs +233 -137
  45. package/lib/index.js +234 -138
  46. package/package.json +7 -7
  47. package/src/PluginSettingsManager.ts +53 -0
  48. package/src/RouteRepository.ts +126 -24
  49. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  50. package/src/__tests__/RouteRepository.test.ts +216 -0
  51. package/src/__tests__/browserChecker.test.ts +61 -0
  52. package/src/__tests__/exports.test.ts +16 -0
  53. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  54. package/src/__tests__/plugin-manager.test.tsx +44 -2
  55. package/src/__tests__/settings-center.test.tsx +40 -1
  56. package/src/collection-manager/field-configure.ts +1 -1
  57. package/src/collection-manager/interfaces/id.ts +1 -1
  58. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  59. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  60. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  61. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  62. package/src/collection-manager/interfaces/obo.tsx +2 -2
  63. package/src/collection-manager/interfaces/oho.tsx +2 -2
  64. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  65. package/src/collection-manager/interfaces/uuid.ts +1 -1
  66. package/src/components/AppComponents.tsx +19 -3
  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 +21 -11
  72. package/src/components/form/TypedVariableInput.tsx +416 -93
  73. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  74. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  75. package/src/components/form/__tests__/TypedVariableInput.test.tsx +268 -9
  76. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  77. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  78. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  79. package/src/components/form/filter/index.ts +2 -0
  80. package/src/components/form/index.tsx +1 -0
  81. package/src/components/index.ts +2 -0
  82. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  83. package/src/flow/__tests__/getKey.test.ts +7 -0
  84. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  85. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  86. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  87. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  88. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  89. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  90. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  91. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  92. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  93. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  94. package/src/flow/actions/afterSuccess.tsx +142 -3
  95. package/src/flow/actions/customVariable.tsx +1 -2
  96. package/src/flow/actions/dataScopeFilter.ts +10 -0
  97. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  98. package/src/flow/actions/index.ts +1 -1
  99. package/src/flow/actions/linkageRules.tsx +123 -35
  100. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  101. package/src/flow/actions/openView.tsx +38 -4
  102. package/src/flow/actions/runjs.tsx +2 -14
  103. package/src/flow/actions/validation.tsx +62 -30
  104. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  105. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  106. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  115. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  119. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  121. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  122. package/src/flow/components/DefaultValue.tsx +1 -2
  123. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  124. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  125. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  126. package/src/flow/components/FlowRoute.tsx +56 -11
  127. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  128. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  129. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  130. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  131. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  132. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  133. package/src/flow/components/fieldAssignOptions.ts +155 -27
  134. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  135. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  136. package/src/flow/index.ts +2 -1
  137. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  138. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  141. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  142. package/src/flow/models/base/ActionModel.tsx +12 -1
  143. package/src/flow/models/base/ActionModelCore.tsx +6 -4
  144. package/src/flow/models/base/GridModel.tsx +38 -7
  145. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  146. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  147. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  148. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  149. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  150. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  151. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  152. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  153. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  154. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  155. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  156. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  157. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  158. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  159. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  160. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  161. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  162. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  163. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  164. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  165. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  166. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  167. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  168. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  169. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  170. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  171. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  172. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  173. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  174. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  175. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  176. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  177. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  178. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  179. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  180. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  181. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  182. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  183. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  185. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  186. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  187. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  188. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  189. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  190. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  191. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  192. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  193. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  194. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  195. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  196. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  197. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  198. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  199. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  200. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  201. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  202. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  203. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  204. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  205. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  206. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  207. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  208. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  209. package/src/flow/utils/formValueDeps.ts +170 -0
  210. package/src/flow-compat/FieldValidation.tsx +122 -60
  211. package/src/flow-compat/Popover.tsx +43 -4
  212. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  213. package/src/index.ts +8 -1
  214. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  215. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  216. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  217. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  218. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  219. package/src/settings-center/plugin-manager/index.tsx +3 -0
  220. package/src/settings-center/utils.tsx +0 -6
@@ -0,0 +1,310 @@
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 from 'react';
13
+ import { createMemoryRouter, RouterProvider } from 'react-router-dom';
14
+ import { describe, expect, it, vi } from 'vitest';
15
+ import { RouteRepository } from '../../../RouteRepository';
16
+ import { AdminLayoutEntryGuard } from './AdminLayoutEntryGuard';
17
+ import type { AdminLayoutModel } from './AdminLayoutModel';
18
+
19
+ describe('AdminLayoutEntryGuard', () => {
20
+ it('should ensure accessible routes for the active layout before resolving the landing route', async () => {
21
+ const engine = new FlowEngine();
22
+ const request = vi.fn().mockResolvedValue({
23
+ data: {
24
+ data: [],
25
+ },
26
+ });
27
+ const routeRepository = new RouteRepository({
28
+ api: {
29
+ request,
30
+ resource: vi.fn(),
31
+ },
32
+ } as never);
33
+ routeRepository.setRoutes([], 'admin-layout-model');
34
+ const deactivateLayout = routeRepository.activateLayout({
35
+ uid: 'mobile-layout-model',
36
+ });
37
+ engine.context.defineProperty('routeRepository', {
38
+ value: routeRepository,
39
+ });
40
+ engine.context.defineProperty('app', {
41
+ value: {
42
+ router: {
43
+ getBasename: () => '',
44
+ },
45
+ },
46
+ });
47
+
48
+ const model = {
49
+ layout: {
50
+ routeName: 'mobile',
51
+ routePath: '/mobile',
52
+ uid: 'mobile-layout-model',
53
+ },
54
+ } as AdminLayoutModel;
55
+ const router = createMemoryRouter(
56
+ [
57
+ {
58
+ path: '/mobile',
59
+ id: 'mobile',
60
+ element: (
61
+ <FlowEngineProvider engine={engine}>
62
+ <AdminLayoutEntryGuard model={model}>
63
+ <div>Mobile layout shell</div>
64
+ </AdminLayoutEntryGuard>
65
+ </FlowEngineProvider>
66
+ ),
67
+ },
68
+ ],
69
+ {
70
+ initialEntries: ['/mobile'],
71
+ },
72
+ );
73
+
74
+ render(<RouterProvider router={router} />);
75
+
76
+ await waitFor(() => {
77
+ expect(request).toHaveBeenCalledWith({
78
+ url: '/desktopRoutes:listAccessible',
79
+ params: {
80
+ tree: true,
81
+ sort: 'sort',
82
+ layout: 'mobile-layout-model',
83
+ },
84
+ });
85
+ });
86
+ expect(screen.getByText('Mobile layout shell')).toBeInTheDocument();
87
+ deactivateLayout();
88
+ });
89
+
90
+ it('should activate the guard layout before loading accessible routes', async () => {
91
+ const engine = new FlowEngine();
92
+ const request = vi.fn().mockResolvedValue({
93
+ data: {
94
+ data: [],
95
+ },
96
+ });
97
+ const routeRepository = new RouteRepository({
98
+ api: {
99
+ request,
100
+ resource: vi.fn(),
101
+ },
102
+ } as never);
103
+ engine.context.defineProperty('routeRepository', {
104
+ value: routeRepository,
105
+ });
106
+ engine.context.defineProperty('app', {
107
+ value: {
108
+ router: {
109
+ getBasename: () => '',
110
+ },
111
+ },
112
+ });
113
+
114
+ const model = {
115
+ layout: {
116
+ routeName: 'mobile',
117
+ routePath: '/mobile',
118
+ uid: 'mobile-layout-model',
119
+ },
120
+ } as AdminLayoutModel;
121
+ const router = createMemoryRouter(
122
+ [
123
+ {
124
+ path: '/mobile',
125
+ id: 'mobile',
126
+ element: (
127
+ <FlowEngineProvider engine={engine}>
128
+ <AdminLayoutEntryGuard model={model}>
129
+ <div>Mobile layout shell</div>
130
+ </AdminLayoutEntryGuard>
131
+ </FlowEngineProvider>
132
+ ),
133
+ },
134
+ ],
135
+ {
136
+ initialEntries: ['/mobile'],
137
+ },
138
+ );
139
+
140
+ render(<RouterProvider router={router} />);
141
+
142
+ await waitFor(() => {
143
+ expect(request).toHaveBeenCalledWith({
144
+ url: '/desktopRoutes:listAccessible',
145
+ params: {
146
+ tree: true,
147
+ sort: 'sort',
148
+ layout: 'mobile-layout-model',
149
+ },
150
+ });
151
+ });
152
+ expect(screen.getByText('Mobile layout shell')).toBeInTheDocument();
153
+ });
154
+
155
+ it('should keep guard stable when layout getter returns a new object with the same fields', async () => {
156
+ const engine = new FlowEngine();
157
+ const request = vi.fn().mockResolvedValue({
158
+ data: {
159
+ data: [],
160
+ },
161
+ });
162
+ const routeRepository = new RouteRepository({
163
+ api: {
164
+ request,
165
+ resource: vi.fn(),
166
+ },
167
+ } as never);
168
+ const activateLayout = vi.spyOn(routeRepository, 'activateLayout');
169
+ engine.context.defineProperty('routeRepository', {
170
+ value: routeRepository,
171
+ });
172
+ engine.context.defineProperty('app', {
173
+ value: {
174
+ router: {
175
+ getBasename: () => '',
176
+ },
177
+ },
178
+ });
179
+
180
+ const model = {
181
+ get layout() {
182
+ return {
183
+ authCheck: true,
184
+ routeName: 'admin',
185
+ routePath: '/admin',
186
+ uid: 'admin-layout-model',
187
+ };
188
+ },
189
+ } as AdminLayoutModel;
190
+ let forceRerender = () => {};
191
+ const GuardShell = () => {
192
+ const [, setVersion] = React.useState(0);
193
+ forceRerender = () => setVersion((version) => version + 1);
194
+ return (
195
+ <AdminLayoutEntryGuard model={model}>
196
+ <div>Admin layout shell</div>
197
+ </AdminLayoutEntryGuard>
198
+ );
199
+ };
200
+ const router = createMemoryRouter(
201
+ [
202
+ {
203
+ path: '/admin',
204
+ id: 'admin',
205
+ element: (
206
+ <FlowEngineProvider engine={engine}>
207
+ <GuardShell />
208
+ </FlowEngineProvider>
209
+ ),
210
+ },
211
+ ],
212
+ {
213
+ initialEntries: ['/admin'],
214
+ },
215
+ );
216
+
217
+ render(<RouterProvider router={router} />);
218
+
219
+ await waitFor(() => {
220
+ expect(screen.getByText('Admin layout shell')).toBeInTheDocument();
221
+ });
222
+ expect(activateLayout).toHaveBeenCalledTimes(1);
223
+
224
+ await act(async () => {
225
+ forceRerender();
226
+ });
227
+
228
+ expect(activateLayout).toHaveBeenCalledTimes(1);
229
+ expect(request).toHaveBeenCalledTimes(1);
230
+ });
231
+
232
+ it('should reactivate the guard when stable layout fields change', async () => {
233
+ const engine = new FlowEngine();
234
+ const request = vi.fn().mockResolvedValue({
235
+ data: {
236
+ data: [],
237
+ },
238
+ });
239
+ const routeRepository = new RouteRepository({
240
+ api: {
241
+ request,
242
+ resource: vi.fn(),
243
+ },
244
+ } as never);
245
+ const activateLayout = vi.spyOn(routeRepository, 'activateLayout');
246
+ engine.context.defineProperty('routeRepository', {
247
+ value: routeRepository,
248
+ });
249
+ engine.context.defineProperty('app', {
250
+ value: {
251
+ router: {
252
+ getBasename: () => '',
253
+ },
254
+ },
255
+ });
256
+
257
+ let layoutUid = 'admin-layout-model';
258
+ const model = {
259
+ get layout() {
260
+ return {
261
+ authCheck: true,
262
+ routeName: 'admin',
263
+ routePath: '/admin',
264
+ uid: layoutUid,
265
+ };
266
+ },
267
+ } as AdminLayoutModel;
268
+ let forceRerender = () => {};
269
+ const GuardShell = () => {
270
+ const [, setVersion] = React.useState(0);
271
+ forceRerender = () => setVersion((version) => version + 1);
272
+ return (
273
+ <AdminLayoutEntryGuard model={model}>
274
+ <div>Admin layout shell</div>
275
+ </AdminLayoutEntryGuard>
276
+ );
277
+ };
278
+ const router = createMemoryRouter(
279
+ [
280
+ {
281
+ path: '/admin',
282
+ id: 'admin',
283
+ element: (
284
+ <FlowEngineProvider engine={engine}>
285
+ <GuardShell />
286
+ </FlowEngineProvider>
287
+ ),
288
+ },
289
+ ],
290
+ {
291
+ initialEntries: ['/admin'],
292
+ },
293
+ );
294
+
295
+ render(<RouterProvider router={router} />);
296
+
297
+ await waitFor(() => {
298
+ expect(screen.getByText('Admin layout shell')).toBeInTheDocument();
299
+ });
300
+ expect(activateLayout).toHaveBeenCalledWith(expect.objectContaining({ uid: 'admin-layout-model' }));
301
+
302
+ await act(async () => {
303
+ layoutUid = 'admin-layout-next';
304
+ forceRerender();
305
+ });
306
+
307
+ expect(activateLayout).toHaveBeenCalledTimes(2);
308
+ expect(activateLayout).toHaveBeenLastCalledWith(expect.objectContaining({ uid: 'admin-layout-next' }));
309
+ });
310
+ });
@@ -13,13 +13,23 @@ import { useLocation, useMatches, useNavigate } from 'react-router-dom';
13
13
  import { NocoBaseDesktopRouteType } from '../../../flow-compat';
14
14
  import { useApp } from '../../../hooks/useApp';
15
15
  import { isLayoutContentRouteName } from '../../../layout-manager/utils';
16
+ import type { LayoutDefinition } from '../../../layout-manager/types';
16
17
  import {
17
18
  findFirstV2LandingRoute,
19
+ getAdminLayoutRoutePath,
18
20
  resolveAdminRouteRuntimeTarget,
21
+ type AdminLayoutRoutePathLike,
19
22
  toRouterNavigationPath,
20
23
  } from './resolveAdminRouteRuntimeTarget';
24
+ import type { AdminLayoutModel } from './AdminLayoutModel';
21
25
 
22
- export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ children }) => {
26
+ type StableAdminEntryLayout = AdminLayoutRoutePathLike &
27
+ Partial<Pick<LayoutDefinition, 'authCheck' | 'routeName' | 'uid'>>;
28
+
29
+ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode; model?: AdminLayoutModel }> = ({
30
+ children,
31
+ model,
32
+ }) => {
23
33
  const flowEngine = useFlowEngine();
24
34
  const app = useApp();
25
35
  const navigate = useNavigate();
@@ -28,24 +38,44 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
28
38
  const [ready, setReady] = useState(false);
29
39
  const replaceTriggeredRef = useRef(false);
30
40
  const routeRepository = flowEngine.context.routeRepository;
41
+ const rawLayout = model?.layout;
42
+ const layoutUid = rawLayout?.uid;
43
+ const layoutRouteNameValue = rawLayout?.routeName;
44
+ const layoutRoutePathValue = rawLayout?.routePath;
45
+ const layoutAuthCheck = rawLayout?.authCheck;
46
+ const layout = useMemo<StableAdminEntryLayout | undefined>(() => {
47
+ if (!layoutUid && !layoutRouteNameValue && !layoutRoutePathValue && typeof layoutAuthCheck === 'undefined') {
48
+ return undefined;
49
+ }
50
+
51
+ return {
52
+ authCheck: layoutAuthCheck,
53
+ routeName: layoutRouteNameValue,
54
+ routePath: layoutRoutePathValue || '',
55
+ uid: layoutUid,
56
+ };
57
+ }, [layoutAuthCheck, layoutRouteNameValue, layoutRoutePathValue, layoutUid]);
58
+ const layoutRoutePath = getAdminLayoutRoutePath(layout);
59
+ const layoutRouteName = layout?.routeName || 'admin';
60
+ const layoutRuntime = useMemo(() => ({ routePath: layoutRoutePath }), [layoutRoutePath]);
31
61
  const isAdminRoot = useMemo(() => {
32
62
  const pathname = toRouterNavigationPath(location.pathname, app.router.getBasename());
33
- return pathname === '/admin';
34
- }, [app, location.pathname]);
63
+ return pathname === layoutRoutePath;
64
+ }, [app, layoutRoutePath, location.pathname]);
35
65
  const pageUid = useMemo(() => {
36
66
  const lastMatch = matches[matches.length - 1];
37
- if (!isLayoutContentRouteName('admin', lastMatch?.id)) {
67
+ if (!isLayoutContentRouteName(layoutRouteName, lastMatch?.id)) {
38
68
  return '';
39
69
  }
40
- const adminMatch = matches.find((match) => match.id === 'admin');
70
+ const adminMatch = matches.find((match) => match.id === layoutRouteName);
41
71
  return (
42
72
  (lastMatch.params?.name as string) ||
43
73
  parsePathnameToViewParams(location.pathname, {
44
- basePath: adminMatch?.pathname || '/admin',
74
+ basePath: adminMatch?.pathname || layoutRoutePath,
45
75
  })[0]?.viewUid ||
46
76
  ''
47
77
  );
48
- }, [location.pathname, matches]);
78
+ }, [layoutRouteName, layoutRoutePath, location.pathname, matches]);
49
79
 
50
80
  useEffect(() => {
51
81
  replaceTriggeredRef.current = false;
@@ -53,6 +83,7 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
53
83
 
54
84
  useEffect(() => {
55
85
  let active = true;
86
+ const deactivateLayout = routeRepository?.activateLayout?.(layout);
56
87
 
57
88
  const run = async () => {
58
89
  setReady(false);
@@ -85,6 +116,7 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
85
116
  const target = resolveAdminRouteRuntimeTarget({
86
117
  app,
87
118
  route: currentRoute,
119
+ layout: layoutRuntime,
88
120
  location: {
89
121
  pathname: window.location.pathname,
90
122
  search: window.location.search,
@@ -117,6 +149,7 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
117
149
  const target = resolveAdminRouteRuntimeTarget({
118
150
  app,
119
151
  route: firstAccessibleRoute,
152
+ layout: layoutRuntime,
120
153
  });
121
154
 
122
155
  if (!target.runtimePath) {
@@ -135,15 +168,18 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode }> = ({ child
135
168
  navigate(toRouterNavigationPath(target.runtimePath, app.router.getBasename()), { replace: true });
136
169
  };
137
170
 
138
- void run();
171
+ run();
139
172
 
140
173
  return () => {
141
174
  active = false;
175
+ deactivateLayout?.();
142
176
  };
143
177
  }, [
144
178
  app,
145
179
  flowEngine,
146
180
  isAdminRoot,
181
+ layout,
182
+ layoutRuntime,
147
183
  location.hash,
148
184
  location.pathname,
149
185
  location.search,
@@ -17,6 +17,7 @@ import {
17
17
  getAdminLayoutMenuMovePositionOptions,
18
18
  } from './AdminLayoutMenuUtils';
19
19
  import { getFlowPageMenuSchema, getPageMenuSchema, isVariable } from './AdminLayoutCompat';
20
+ import { joinAdminLayoutRoutePath, type AdminLayoutRoutePathLike } from './resolveAdminRouteRuntimeTarget';
20
21
 
21
22
  const buildLinkSettingSchema = (t: (title: any) => any) => ({
22
23
  href: {
@@ -298,6 +299,7 @@ export const matchesRoutePath = (
298
299
  route: NocoBaseDesktopRoute | undefined,
299
300
  pathname: string,
300
301
  basename = '/',
302
+ layout?: AdminLayoutRoutePathLike | null,
301
303
  ): boolean => {
302
304
  if (!route) {
303
305
  return false;
@@ -310,8 +312,8 @@ export const matchesRoutePath = (
310
312
  : pathname;
311
313
 
312
314
  const candidates = [
313
- route.id != null ? `/admin/${route.id}` : null,
314
- route.schemaUid ? `/admin/${route.schemaUid}` : null,
315
+ route.id != null ? joinAdminLayoutRoutePath(layout, route.id) : null,
316
+ route.schemaUid ? joinAdminLayoutRoutePath(layout, route.schemaUid) : null,
315
317
  ].filter(Boolean) as string[];
316
318
 
317
319
  if (
@@ -321,7 +323,7 @@ export const matchesRoutePath = (
321
323
  }
322
324
 
323
325
  return Array.isArray(route.children)
324
- ? route.children.some((child) => matchesRoutePath(child, normalizedPathname, '/'))
326
+ ? route.children.some((child) => matchesRoutePath(child, normalizedPathname, '/', layout))
325
327
  : false;
326
328
  };
327
329
 
@@ -42,9 +42,11 @@ import {
42
42
  } from './AdminLayoutMenuFlowUtils';
43
43
  import { ADMIN_LAYOUT_MODEL_UID } from './constants';
44
44
  import {
45
+ type AdminLayoutRoutePathLike,
45
46
  findFirstAccessiblePageRoute,
46
47
  findFirstV2LandingRoute,
47
48
  isV2AdminRuntime,
49
+ joinAdminLayoutRoutePath,
48
50
  resolveAdminRouteRuntimeTarget,
49
51
  toRouterNavigationPath,
50
52
  } from './resolveAdminRouteRuntimeTarget';
@@ -64,6 +66,26 @@ const omitMenuRuntimeProps = (props: Record<string, unknown> = {}) => {
64
66
  return persistableProps;
65
67
  };
66
68
 
69
+ type MenuRouteTreeRefreshableModel = FlowModel & {
70
+ refreshMenuRouteTree?: () => void;
71
+ };
72
+
73
+ function isMenuRouteTreeRefreshableModel(model: FlowModel | undefined): model is MenuRouteTreeRefreshableModel {
74
+ return typeof (model as MenuRouteTreeRefreshableModel | undefined)?.refreshMenuRouteTree === 'function';
75
+ }
76
+
77
+ function getOwnerMenuRouteTreeRefreshModel(model: FlowModel) {
78
+ let current = model.parent as FlowModel | undefined;
79
+ while (current) {
80
+ if (isMenuRouteTreeRefreshableModel(current)) {
81
+ return current;
82
+ }
83
+ current = current.parent as FlowModel | undefined;
84
+ }
85
+
86
+ return undefined;
87
+ }
88
+
67
89
  export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStructure> {
68
90
  private creationPersisted = false;
69
91
  private persistedStateHydrated = false;
@@ -129,6 +151,24 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
129
151
  return this.context.routeRepository;
130
152
  }
131
153
 
154
+ getLayout(): AdminLayoutRoutePathLike | undefined {
155
+ const contextLayout = this.context.layout as AdminLayoutRoutePathLike | undefined;
156
+ if (contextLayout?.routePath) {
157
+ return contextLayout;
158
+ }
159
+
160
+ let current: FlowModel | undefined = this;
161
+ while (current) {
162
+ const propsLayout = current.props?.layout as AdminLayoutRoutePathLike | undefined;
163
+ if (propsLayout?.routePath) {
164
+ return propsLayout;
165
+ }
166
+ current = current.parent as FlowModel | undefined;
167
+ }
168
+
169
+ return undefined;
170
+ }
171
+
132
172
  hasPersistedMenuInstanceFlowFlag(route = this.getRoute()) {
133
173
  return route?.options?.hasPersistedMenuInstanceFlow === true;
134
174
  }
@@ -280,7 +320,12 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
280
320
  const previous = this.hidden;
281
321
  super.setHidden(value);
282
322
  if (previous !== this.hidden) {
283
- (this.flowEngine.getModel?.(ADMIN_LAYOUT_MODEL_UID) as any)?.refreshMenuRouteTree?.();
323
+ const defaultLayoutModel = this.flowEngine.getModel?.(ADMIN_LAYOUT_MODEL_UID);
324
+ const refreshableModel =
325
+ getOwnerMenuRouteTreeRefreshModel(this) ||
326
+ (isMenuRouteTreeRefreshableModel(defaultLayoutModel) ? defaultLayoutModel : undefined);
327
+
328
+ refreshableModel?.refreshMenuRouteTree?.();
284
329
  }
285
330
  }
286
331
 
@@ -533,9 +578,10 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
533
578
  return false;
534
579
  }
535
580
 
581
+ const layout = this.getLayout();
536
582
  const allAccessRoutes = this.getRouteRepository().listAccessible();
537
583
  const pathname = this.context.location?.pathname || window.location.pathname;
538
- const shouldNavigate = matchesRoutePath(route, pathname, this.context.router?.basename);
584
+ const shouldNavigate = matchesRoutePath(route, pathname, this.context.router?.basename, layout);
539
585
  const prevSibling = findPrevSiblingRoute(allAccessRoutes, route);
540
586
  const nextSibling = findNextSiblingRoute(allAccessRoutes, route);
541
587
 
@@ -554,6 +600,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
554
600
  const target = resolveAdminRouteRuntimeTarget({
555
601
  app: this.context.app,
556
602
  route: sibling,
603
+ layout,
557
604
  });
558
605
 
559
606
  if (!target.runtimePath) {
@@ -584,6 +631,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
584
631
 
585
632
  const shouldShowIconInTitle = shouldRenderIconInTitle({ depth, isMobile: options.isMobile });
586
633
  const { name, icon } = buildMenuTitleWithIcon(route, options.t, shouldShowIconInTitle);
634
+ const layout = this.getLayout();
587
635
 
588
636
  if (route.type === NocoBaseDesktopRouteType.link) {
589
637
  const identity = route.id ?? route.schemaUid ?? route.menuSchemaUid ?? route.title ?? this.uid;
@@ -591,7 +639,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
591
639
  return {
592
640
  name,
593
641
  icon,
594
- path: getAdminLayoutMenuVirtualPath('link', identity),
642
+ path: getAdminLayoutMenuVirtualPath('link', identity, layout),
595
643
  hideInMenu: route.hideInMenu,
596
644
  _route: route,
597
645
  _parentRoute: parentRoute,
@@ -604,6 +652,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
604
652
  const runtimeTarget = resolveAdminRouteRuntimeTarget({
605
653
  app: this.context.app,
606
654
  route,
655
+ layout,
607
656
  });
608
657
 
609
658
  if (runtimeTarget.reason === 'unsupportedV2Runtime') {
@@ -611,14 +660,14 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
611
660
  }
612
661
 
613
662
  const path = route.schemaUid
614
- ? `/admin/${route.schemaUid}`
615
- : getAdminLayoutMenuVirtualPath('link', `${this.uid}-invalid`);
663
+ ? joinAdminLayoutRoutePath(layout, route.schemaUid)
664
+ : getAdminLayoutMenuVirtualPath('link', `${this.uid}-invalid`, layout);
616
665
 
617
666
  return {
618
667
  name,
619
668
  icon,
620
669
  path,
621
- redirect: route.schemaUid ? `/admin/${route.schemaUid}` : undefined,
670
+ redirect: route.schemaUid ? joinAdminLayoutRoutePath(layout, route.schemaUid) : undefined,
622
671
  hideInMenu: route.hideInMenu,
623
672
  disabled: !runtimeTarget.runtimePath,
624
673
  _runtimePath: runtimeTarget.runtimePath,
@@ -644,7 +693,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
644
693
  .filter(Boolean) || [];
645
694
 
646
695
  if (options.designable && depth === 0) {
647
- children.push(getAdminLayoutMenuInitializerButton('schema-initializer-Menu-side', this, route));
696
+ children.push(getAdminLayoutMenuInitializerButton('schema-initializer-Menu-side', this, route, layout));
648
697
  }
649
698
 
650
699
  const landingRoute = isV2AdminRuntime(this.context.app)
@@ -653,14 +702,17 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
653
702
  const runtimeTarget = resolveAdminRouteRuntimeTarget({
654
703
  app: this.context.app,
655
704
  route: isV2AdminRuntime(this.context.app) && landingRoute ? landingRoute : route,
705
+ layout,
656
706
  });
657
707
 
658
708
  const groupRoute: AdminLayoutMenuNode = {
659
709
  name,
660
710
  icon,
661
- path: `/admin/${route.id}`,
711
+ path: joinAdminLayoutRoutePath(layout, route.id),
662
712
  redirect:
663
- children[0]?.key === 'x-designer-button' ? undefined : `/admin/${landingRoute?.schemaUid || route.id}`,
713
+ children[0]?.key === 'x-designer-button'
714
+ ? undefined
715
+ : joinAdminLayoutRoutePath(layout, landingRoute?.schemaUid || route.id),
664
716
  hideInMenu: route.hideInMenu,
665
717
  _runtimePath: runtimeTarget.runtimePath,
666
718
  _navigationMode: runtimeTarget.navigationMode,