@nocobase/client-v2 2.1.0-alpha.40 → 2.1.0-alpha.45

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 (278) hide show
  1. package/es/Application.d.ts +7 -0
  2. package/es/BaseApplication.d.ts +13 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterface.d.ts +51 -15
  5. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +82 -3
  6. package/es/collection-manager/field-configure.d.ts +80 -0
  7. package/es/collection-manager/field-validation.d.ts +43 -0
  8. package/es/collection-manager/filter-operators/index.d.ts +46 -0
  9. package/es/collection-manager/filter-operators/operators.d.ts +30 -0
  10. package/es/collection-manager/interfaces/checkbox.d.ts +1 -41
  11. package/es/collection-manager/interfaces/checkboxGroup.d.ts +12 -44
  12. package/es/collection-manager/interfaces/collection.d.ts +12 -51
  13. package/es/collection-manager/interfaces/color.d.ts +1 -16
  14. package/es/collection-manager/interfaces/createdAt.d.ts +1 -44
  15. package/es/collection-manager/interfaces/createdBy.d.ts +0 -4
  16. package/es/collection-manager/interfaces/dateOnly.d.ts +7 -44
  17. package/es/collection-manager/interfaces/datetime.d.ts +1 -44
  18. package/es/collection-manager/interfaces/datetimeNoTz.d.ts +1 -44
  19. package/es/collection-manager/interfaces/email.d.ts +1 -29
  20. package/es/collection-manager/interfaces/id.d.ts +1 -16
  21. package/es/collection-manager/interfaces/index.d.ts +2 -3
  22. package/es/collection-manager/interfaces/input.d.ts +1 -102
  23. package/es/collection-manager/interfaces/integer.d.ts +1 -95
  24. package/es/collection-manager/interfaces/json.d.ts +16 -7
  25. package/es/collection-manager/interfaces/m2m.d.ts +11 -19
  26. package/es/collection-manager/interfaces/m2o.d.ts +11 -19
  27. package/es/collection-manager/interfaces/markdown.d.ts +1 -63
  28. package/es/collection-manager/interfaces/multipleSelect.d.ts +12 -44
  29. package/es/collection-manager/interfaces/nanoid.d.ts +1 -34
  30. package/es/collection-manager/interfaces/number.d.ts +1 -87
  31. package/es/collection-manager/interfaces/o2m.d.ts +12 -24
  32. package/es/collection-manager/interfaces/obo.d.ts +207 -0
  33. package/es/collection-manager/interfaces/oho.d.ts +207 -0
  34. package/es/collection-manager/interfaces/password.d.ts +1 -56
  35. package/es/collection-manager/interfaces/percent.d.ts +1 -84
  36. package/es/collection-manager/interfaces/phone.d.ts +1 -25
  37. package/es/collection-manager/interfaces/properties/index.d.ts +0 -28
  38. package/es/collection-manager/interfaces/radioGroup.d.ts +1 -29
  39. package/es/collection-manager/interfaces/richText.d.ts +1 -63
  40. package/es/collection-manager/interfaces/select.d.ts +12 -44
  41. package/es/collection-manager/interfaces/snowflake-id.d.ts +1 -34
  42. package/es/collection-manager/interfaces/tableoid.d.ts +1 -10
  43. package/es/collection-manager/interfaces/textarea.d.ts +1 -51
  44. package/es/collection-manager/interfaces/time.d.ts +1 -16
  45. package/es/collection-manager/interfaces/types.d.ts +3 -12
  46. package/es/collection-manager/interfaces/unixTimestamp.d.ts +1 -44
  47. package/es/collection-manager/interfaces/updatedAt.d.ts +1 -44
  48. package/es/collection-manager/interfaces/updatedBy.d.ts +0 -4
  49. package/es/collection-manager/interfaces/url.d.ts +1 -20
  50. package/es/collection-manager/interfaces/uuid.d.ts +1 -34
  51. package/es/collection-manager/template-fields.d.ts +53 -0
  52. package/es/components/KeepAlive.d.ts +22 -0
  53. package/es/components/RouterBridge.d.ts +9 -0
  54. package/es/components/form/DialogFormLayout.d.ts +5 -29
  55. package/es/components/form/VariableInput.d.ts +53 -2
  56. package/es/components/form/filter/CollectionFilter.d.ts +49 -0
  57. package/es/components/form/filter/CollectionFilterItem.d.ts +49 -0
  58. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  59. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  60. package/es/components/form/filter/index.d.ts +11 -0
  61. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  62. package/es/components/form/index.d.ts +1 -0
  63. package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
  64. package/es/data-source/index.d.ts +9 -0
  65. package/es/flow/FlowPage.d.ts +2 -1
  66. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  67. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  68. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  69. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  70. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  71. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
  72. package/es/flow/admin-shell/admin-layout/index.d.ts +3 -0
  73. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
  74. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  75. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  76. package/es/flow/components/FlowRoute.d.ts +10 -1
  77. package/es/flow/components/filter/index.d.ts +2 -0
  78. package/es/flow/components/filter/useFilterOptions.d.ts +66 -0
  79. package/es/flow/index.d.ts +4 -0
  80. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  81. package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +84 -0
  82. package/es/flow/models/blocks/assign-form/index.d.ts +1 -0
  83. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  84. package/es/flow/models/blocks/form/FormActionModel.d.ts +9 -2
  85. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  86. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  87. package/es/flow-compat/passwordUtils.d.ts +1 -1
  88. package/es/index.d.ts +6 -0
  89. package/es/index.mjs +552 -459
  90. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  91. package/es/layout-manager/LayoutManager.d.ts +22 -0
  92. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  93. package/es/layout-manager/index.d.ts +13 -0
  94. package/es/layout-manager/types.d.ts +20 -0
  95. package/es/layout-manager/utils.d.ts +14 -0
  96. package/es/nocobase-buildin-plugin/index.d.ts +3 -10
  97. package/es/settings-center/index.d.ts +1 -1
  98. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  99. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  100. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  101. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  102. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  103. package/lib/index.js +552 -459
  104. package/package.json +8 -7
  105. package/src/Application.tsx +51 -12
  106. package/src/BaseApplication.tsx +32 -0
  107. package/src/PluginSettingsManager.ts +1 -1
  108. package/src/RouterManager.tsx +17 -1
  109. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  110. package/src/__tests__/app.test.tsx +17 -1
  111. package/src/__tests__/globalDeps.test.ts +1 -0
  112. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  113. package/src/__tests__/plugin-manager.test.tsx +177 -0
  114. package/src/__tests__/settings-center.test.tsx +24 -2
  115. package/src/collection-field-interface/CollectionFieldInterface.ts +71 -77
  116. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +201 -4
  117. package/src/collection-manager/field-configure.ts +548 -0
  118. package/src/collection-manager/field-validation.ts +195 -0
  119. package/src/collection-manager/filter-operators/index.ts +176 -0
  120. package/src/collection-manager/{interfaces/properties → filter-operators}/operators.ts +24 -13
  121. package/src/collection-manager/interfaces/checkbox.ts +2 -9
  122. package/src/collection-manager/interfaces/checkboxGroup.ts +2 -10
  123. package/src/collection-manager/interfaces/collection.ts +2 -15
  124. package/src/collection-manager/interfaces/color.ts +2 -2
  125. package/src/collection-manager/interfaces/createdAt.ts +2 -2
  126. package/src/collection-manager/interfaces/createdBy.ts +1 -12
  127. package/src/collection-manager/interfaces/dateOnly.ts +8 -2
  128. package/src/collection-manager/interfaces/datetime.ts +2 -2
  129. package/src/collection-manager/interfaces/datetimeNoTz.ts +2 -2
  130. package/src/collection-manager/interfaces/email.ts +2 -9
  131. package/src/collection-manager/interfaces/id.ts +1 -2
  132. package/src/collection-manager/interfaces/index.ts +2 -3
  133. package/src/collection-manager/interfaces/input.ts +2 -133
  134. package/src/collection-manager/interfaces/integer.ts +2 -71
  135. package/src/collection-manager/interfaces/json.tsx +17 -11
  136. package/src/collection-manager/interfaces/m2m.tsx +0 -21
  137. package/src/collection-manager/interfaces/m2o.tsx +0 -22
  138. package/src/collection-manager/interfaces/markdown.ts +2 -51
  139. package/src/collection-manager/interfaces/multipleSelect.ts +2 -14
  140. package/src/collection-manager/interfaces/nanoid.ts +2 -2
  141. package/src/collection-manager/interfaces/number.ts +2 -85
  142. package/src/collection-manager/interfaces/o2m.tsx +1 -22
  143. package/src/collection-manager/interfaces/obo.tsx +145 -0
  144. package/src/collection-manager/interfaces/oho.tsx +145 -0
  145. package/src/collection-manager/interfaces/password.ts +2 -44
  146. package/src/collection-manager/interfaces/percent.ts +2 -74
  147. package/src/collection-manager/interfaces/phone.ts +2 -2
  148. package/src/collection-manager/interfaces/properties/index.ts +0 -133
  149. package/src/collection-manager/interfaces/radioGroup.ts +2 -2
  150. package/src/collection-manager/interfaces/richText.ts +2 -51
  151. package/src/collection-manager/interfaces/select.ts +2 -14
  152. package/src/collection-manager/interfaces/snowflake-id.ts +2 -2
  153. package/src/collection-manager/interfaces/tableoid.ts +1 -2
  154. package/src/collection-manager/interfaces/textarea.ts +2 -51
  155. package/src/collection-manager/interfaces/time.ts +2 -2
  156. package/src/collection-manager/interfaces/types.ts +4 -12
  157. package/src/collection-manager/interfaces/unixTimestamp.tsx +2 -2
  158. package/src/collection-manager/interfaces/updatedAt.ts +2 -2
  159. package/src/collection-manager/interfaces/updatedBy.ts +1 -12
  160. package/src/collection-manager/interfaces/url.ts +2 -4
  161. package/src/collection-manager/interfaces/uuid.ts +2 -2
  162. package/src/collection-manager/template-fields.ts +109 -0
  163. package/src/components/KeepAlive.tsx +131 -0
  164. package/src/components/README.md +90 -6
  165. package/src/components/README.zh-CN.md +90 -7
  166. package/src/components/RouterBridge.tsx +28 -4
  167. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  168. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  169. package/src/components/form/DialogFormLayout.tsx +5 -29
  170. package/src/components/form/VariableInput.tsx +101 -28
  171. package/src/components/form/__tests__/VariableInput.test.ts +85 -0
  172. package/src/components/form/filter/CollectionFilter.tsx +111 -0
  173. package/src/components/form/filter/CollectionFilterItem.tsx +184 -0
  174. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  175. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  176. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +247 -0
  177. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  178. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  179. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  180. package/src/components/form/filter/index.ts +13 -0
  181. package/src/components/form/filter/useFilterActionProps.ts +203 -0
  182. package/src/components/form/index.tsx +1 -0
  183. package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
  184. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  185. package/src/data-source/index.ts +10 -0
  186. package/src/flow/FlowPage.tsx +35 -7
  187. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  188. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  189. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  190. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  191. package/src/flow/actions/aclCheck.tsx +4 -0
  192. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  193. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  194. package/src/flow/actions/linkageRules.tsx +122 -0
  195. package/src/flow/actions/openView.tsx +28 -4
  196. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  197. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  198. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  199. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  200. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +35 -7
  201. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  202. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  203. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  204. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  205. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
  206. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  207. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  208. package/src/flow/admin-shell/admin-layout/index.ts +3 -0
  209. package/src/flow/admin-shell/admin-layout/useApplications.tsx +34 -1
  210. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  211. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  212. package/src/flow/components/AdminLayout.tsx +4 -154
  213. package/src/flow/components/FlowRoute.tsx +105 -15
  214. package/src/flow/components/filter/index.ts +3 -0
  215. package/src/flow/components/filter/useFilterOptions.ts +102 -0
  216. package/src/flow/index.ts +4 -0
  217. package/src/flow/models/actions/UpdateRecordActionModel.tsx +14 -95
  218. package/src/flow/models/actions/UpdateRecordActionUtils.ts +4 -7
  219. package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +26 -1
  220. package/src/flow/models/base/ActionModel.tsx +8 -1
  221. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  222. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  223. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  224. package/src/flow/models/base/RouteModel.tsx +1 -1
  225. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +63 -2
  226. package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +206 -0
  227. package/src/flow/models/blocks/assign-form/index.ts +1 -0
  228. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  229. package/src/flow/models/blocks/form/FormActionModel.tsx +30 -3
  230. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  231. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  232. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +71 -0
  233. package/src/flow/models/blocks/form/submitHandler.ts +8 -1
  234. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  235. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  236. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  237. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  238. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  239. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  240. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  241. package/src/flow/models/fields/CollectionSelectorFieldModel.tsx +8 -2
  242. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +8 -2
  243. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  245. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  246. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  247. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  248. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  249. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  250. package/src/index.ts +6 -0
  251. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  252. package/src/layout-manager/LayoutManager.tsx +185 -0
  253. package/src/layout-manager/LayoutRoute.tsx +138 -0
  254. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  255. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  256. package/src/layout-manager/index.ts +14 -0
  257. package/src/layout-manager/types.ts +22 -0
  258. package/src/layout-manager/utils.ts +37 -0
  259. package/src/nocobase-buildin-plugin/index.tsx +69 -67
  260. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  261. package/src/settings-center/index.ts +1 -1
  262. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  263. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  264. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  265. package/src/settings-center/plugin-manager/index.tsx +254 -0
  266. package/src/settings-center/plugin-manager/types.ts +35 -0
  267. package/src/settings-center/utils.tsx +8 -1
  268. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  269. package/src/theme/globalStyles.ts +10 -0
  270. package/src/utils/globalDeps.ts +2 -0
  271. package/es/collection-manager/interfaces/linkTo.d.ts +0 -90
  272. package/es/collection-manager/interfaces/o2o.d.ts +0 -621
  273. package/es/collection-manager/interfaces/properties/operators.d.ts +0 -294
  274. package/es/collection-manager/interfaces/subTable.d.ts +0 -172
  275. package/src/collection-manager/interfaces/linkTo.ts +0 -120
  276. package/src/collection-manager/interfaces/o2o.tsx +0 -561
  277. package/src/collection-manager/interfaces/subTable.ts +0 -218
  278. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -0,0 +1,473 @@
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, observer } from '@nocobase/flow-engine';
11
+ import { render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { createMemoryRouter, Outlet, RouterProvider, useOutlet } from 'react-router-dom';
14
+ import { describe, expect, it } from 'vitest';
15
+ import { BaseLayoutModel } from '../../flow/admin-shell/BaseLayoutModel';
16
+ import { LayoutContentRoute } from '../LayoutContentRoute';
17
+ import { LayoutRoute } from '../LayoutRoute';
18
+ import type { LayoutDefinition } from '../types';
19
+ import {
20
+ getLayoutPageRouteName,
21
+ getLayoutPageTabRouteName,
22
+ getLayoutPageTabViewRouteName,
23
+ getLayoutPageViewRouteName,
24
+ } from '../utils';
25
+
26
+ class TestLayoutModel extends BaseLayoutModel {
27
+ render() {
28
+ return <div data-testid="layout-route">{this.layout.routeName}</div>;
29
+ }
30
+ }
31
+
32
+ const GatedLayoutComponent = observer((props: { model: BaseLayoutModel }) => {
33
+ const { model } = props;
34
+ const outlet = useOutlet();
35
+ const pageUid = model.getPageUidFromLayoutRoute(model.currentLayoutRoute);
36
+
37
+ if (!pageUid) {
38
+ return <div data-testid="layout-page-uid">missing</div>;
39
+ }
40
+
41
+ return (
42
+ <div>
43
+ <div data-testid="layout-page-uid">{pageUid}</div>
44
+ {outlet}
45
+ </div>
46
+ );
47
+ });
48
+
49
+ class GatedLayoutModel extends BaseLayoutModel {
50
+ render() {
51
+ return <GatedLayoutComponent model={this} />;
52
+ }
53
+ }
54
+
55
+ const layout: LayoutDefinition = {
56
+ routeName: 'test',
57
+ routePath: '/test',
58
+ rootRouteName: 'test',
59
+ uid: 'test-layout-model',
60
+ layoutModelClass: 'TestLayoutModel',
61
+ rootPageModelClass: 'TestRootPageModel',
62
+ childPageModelClass: 'TestChildPageModel',
63
+ authCheck: true,
64
+ };
65
+
66
+ describe('LayoutRoute', () => {
67
+ it('creates layout model from registered string class and injects layout definition', async () => {
68
+ const engine = new FlowEngine();
69
+ engine.registerModels({ TestLayoutModel });
70
+ engine.context.defineProperty('app', {
71
+ value: {
72
+ layoutManager: {
73
+ getLayout: () => layout,
74
+ },
75
+ },
76
+ });
77
+
78
+ const router = createMemoryRouter(
79
+ [
80
+ {
81
+ id: layout.routeName,
82
+ path: layout.routePath,
83
+ element: <LayoutRoute layoutRouteName="test" />,
84
+ },
85
+ ],
86
+ {
87
+ initialEntries: ['/test'],
88
+ },
89
+ );
90
+
91
+ render(
92
+ <FlowEngineProvider engine={engine}>
93
+ <RouterProvider router={router} />
94
+ </FlowEngineProvider>,
95
+ );
96
+
97
+ expect(await screen.findByTestId('layout-route')).toHaveTextContent('test');
98
+ const model = engine.getModel<TestLayoutModel>('test-layout-model');
99
+ expect(model).toBeInstanceOf(TestLayoutModel);
100
+ expect(model.layout).toMatchObject({
101
+ routeName: 'test',
102
+ routePath: '/test',
103
+ rootRouteName: 'test',
104
+ rootPageModelClass: 'TestRootPageModel',
105
+ childPageModelClass: 'TestChildPageModel',
106
+ });
107
+ });
108
+
109
+ it('syncs nested page route before the layout renders its outlet', async () => {
110
+ const nestedLayout: LayoutDefinition = {
111
+ ...layout,
112
+ routeName: 'admin.settings.publicForms.layout',
113
+ routePath: '',
114
+ rootRouteName: 'admin',
115
+ uid: 'gated-layout-model',
116
+ layoutModelClass: 'GatedLayoutModel',
117
+ };
118
+ const engine = new FlowEngine();
119
+ engine.registerModels({ GatedLayoutModel });
120
+ engine.context.defineProperty('app', {
121
+ value: {
122
+ layoutManager: {
123
+ getLayout: () => nestedLayout,
124
+ },
125
+ },
126
+ });
127
+ const router = createMemoryRouter(
128
+ [
129
+ {
130
+ id: 'admin.settings',
131
+ path: '/admin/settings',
132
+ element: <Outlet />,
133
+ children: [
134
+ {
135
+ id: 'admin.settings.publicForms',
136
+ path: 'public-forms',
137
+ element: <Outlet />,
138
+ children: [
139
+ {
140
+ id: nestedLayout.routeName,
141
+ path: '',
142
+ element: <LayoutRoute layoutRouteName={nestedLayout.routeName} />,
143
+ children: [
144
+ {
145
+ id: getLayoutPageViewRouteName(nestedLayout.routeName),
146
+ path: ':name/view/*',
147
+ element: <div data-testid="layout-child-outlet">child page</div>,
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ },
155
+ ],
156
+ {
157
+ initialEntries: ['/admin/settings/public-forms/form-1/view/popup'],
158
+ },
159
+ );
160
+
161
+ render(
162
+ <FlowEngineProvider engine={engine}>
163
+ <RouterProvider router={router} />
164
+ </FlowEngineProvider>,
165
+ );
166
+
167
+ expect(await screen.findByTestId('layout-page-uid')).toHaveTextContent('form-1');
168
+ expect(await screen.findByTestId('layout-child-outlet')).toHaveTextContent('child page');
169
+ });
170
+
171
+ it('keeps local layout route while parent layout route switches within the same page view stack', async () => {
172
+ const clearEvents: Array<{ routePathname?: string; before: string | null; after: string | null }> = [];
173
+
174
+ class TrackingLayoutModel extends TestLayoutModel {
175
+ clearLayoutRoute(routeLike?: Parameters<BaseLayoutModel['clearLayoutRoute']>[0]) {
176
+ const event: { routePathname?: string; before: string | null; after: string | null } = {
177
+ routePathname: routeLike?.pathname,
178
+ before: this.currentLayoutRoute?.pathname || null,
179
+ after: null,
180
+ };
181
+ super.clearLayoutRoute(routeLike);
182
+ event.after = this.currentLayoutRoute?.pathname || null;
183
+ clearEvents.push(event);
184
+ }
185
+ }
186
+
187
+ const engine = new FlowEngine();
188
+ engine.registerModels({ TrackingLayoutModel });
189
+ const trackingLayout: LayoutDefinition = {
190
+ ...layout,
191
+ layoutModelClass: 'TrackingLayoutModel',
192
+ };
193
+ engine.context.defineProperty('app', {
194
+ value: {
195
+ layoutManager: {
196
+ getLayout: () => trackingLayout,
197
+ },
198
+ },
199
+ });
200
+ const router = createMemoryRouter(
201
+ [
202
+ {
203
+ id: layout.routeName,
204
+ path: layout.routePath,
205
+ element: <LayoutRoute layoutRouteName={layout.routeName} />,
206
+ children: [
207
+ {
208
+ id: getLayoutPageRouteName(layout.routeName),
209
+ path: ':name',
210
+ element: <div data-testid="page-route">page</div>,
211
+ },
212
+ {
213
+ id: getLayoutPageViewRouteName(layout.routeName),
214
+ path: ':name/view/*',
215
+ element: <div data-testid="page-view-route">page view</div>,
216
+ },
217
+ ],
218
+ },
219
+ ],
220
+ {
221
+ initialEntries: ['/test/page-1'],
222
+ },
223
+ );
224
+
225
+ render(
226
+ <FlowEngineProvider engine={engine}>
227
+ <RouterProvider router={router} />
228
+ </FlowEngineProvider>,
229
+ );
230
+
231
+ const model = await waitFor(() => {
232
+ const layoutModel = engine.getModel<TrackingLayoutModel>(layout.uid);
233
+ expect(layoutModel?.currentLayoutRoute).toMatchObject({
234
+ type: 'page',
235
+ pageUid: 'page-1',
236
+ pathname: '/test/page-1',
237
+ });
238
+ return layoutModel as TrackingLayoutModel;
239
+ });
240
+
241
+ await router.navigate('/test/page-1/view/popup');
242
+
243
+ await waitFor(() => {
244
+ expect(model.currentLayoutRoute).toMatchObject({
245
+ type: 'page',
246
+ pageUid: 'page-1',
247
+ pathname: '/test/page-1/view/popup',
248
+ });
249
+ });
250
+
251
+ expect(clearEvents).not.toContainEqual({
252
+ routePathname: '/test/page-1',
253
+ before: '/test/page-1',
254
+ after: null,
255
+ });
256
+ });
257
+ });
258
+
259
+ describe('LayoutContentRoute', () => {
260
+ function setup(
261
+ initialEntry: string,
262
+ currentLayout: LayoutDefinition = layout,
263
+ ModelClass: typeof TestLayoutModel = TestLayoutModel,
264
+ ) {
265
+ const engine = new FlowEngine();
266
+ engine.registerModels({ TestLayoutModel, [ModelClass.name]: ModelClass });
267
+ engine.context.defineProperty('routeRepository', {
268
+ value: {
269
+ refreshAccessible: () => Promise.resolve(),
270
+ isAccessibleLoaded: () => true,
271
+ ensureAccessibleLoaded: () => Promise.resolve(),
272
+ getRouteBySchemaUid: () => ({ type: 'flowPage', schemaUid: 'page-1' }),
273
+ },
274
+ });
275
+ engine.context.defineProperty('app', {
276
+ value: {
277
+ getPublicPath: () => '/',
278
+ layoutManager: {
279
+ getLayout: () => currentLayout,
280
+ },
281
+ router: {
282
+ getBasename: () => '',
283
+ },
284
+ },
285
+ });
286
+ const model = engine.createModel<TestLayoutModel>({
287
+ uid: layout.uid,
288
+ use: ModelClass,
289
+ props: {
290
+ layout: currentLayout,
291
+ },
292
+ });
293
+ const layoutRoute = {
294
+ id: currentLayout.routeName,
295
+ path: currentLayout.routePath,
296
+ element: <Outlet />,
297
+ children: [
298
+ {
299
+ id: getLayoutPageRouteName(currentLayout.routeName),
300
+ path: ':name',
301
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
302
+ },
303
+ {
304
+ id: getLayoutPageTabRouteName(currentLayout.routeName),
305
+ path: ':name/tab/:tabUid',
306
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
307
+ },
308
+ {
309
+ id: getLayoutPageViewRouteName(currentLayout.routeName),
310
+ path: ':name/view/*',
311
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
312
+ },
313
+ {
314
+ id: getLayoutPageTabViewRouteName(currentLayout.routeName),
315
+ path: ':name/tab/:tabUid/view/*',
316
+ element: <LayoutContentRoute layoutRouteName={currentLayout.routeName} />,
317
+ },
318
+ ],
319
+ };
320
+ let routes;
321
+ if (currentLayout.routeName === 'admin.settings.publicForms.layout') {
322
+ routes = [
323
+ {
324
+ id: 'admin.settings',
325
+ path: '/admin/settings',
326
+ element: <Outlet />,
327
+ children: [
328
+ {
329
+ id: 'admin.settings.publicForms',
330
+ path: '/admin/settings/public-forms',
331
+ element: <Outlet />,
332
+ children: [layoutRoute],
333
+ },
334
+ ],
335
+ },
336
+ ];
337
+ } else if (currentLayout.routeName.includes('.')) {
338
+ routes = [
339
+ {
340
+ id: 'admin.settings',
341
+ path: '/admin/settings',
342
+ element: <Outlet />,
343
+ children: [layoutRoute],
344
+ },
345
+ ];
346
+ } else {
347
+ routes = [layoutRoute];
348
+ }
349
+ const router = createMemoryRouter(routes, {
350
+ initialEntries: [initialEntry],
351
+ });
352
+
353
+ render(
354
+ <FlowEngineProvider engine={engine}>
355
+ <RouterProvider router={router} />
356
+ </FlowEngineProvider>,
357
+ );
358
+
359
+ return { model, router };
360
+ }
361
+
362
+ it('parses page route from standard layout route', async () => {
363
+ const { model } = setup('/test/page-1/tab/tab-1/view/popup');
364
+
365
+ await waitFor(() => {
366
+ expect(model.currentLayoutRoute).toMatchObject({
367
+ type: 'page',
368
+ basePathname: '/test',
369
+ pageUid: 'page-1',
370
+ tabUid: 'tab-1',
371
+ viewStack: [{ viewUid: 'page-1', tabUid: 'tab-1' }, { viewUid: 'popup' }],
372
+ });
373
+ });
374
+ });
375
+
376
+ it('parses nested layout route by matched layout pathname', async () => {
377
+ const nestedLayout: LayoutDefinition = {
378
+ ...layout,
379
+ routeName: 'admin.settings.publicForms',
380
+ routePath: 'public-forms',
381
+ rootRouteName: 'admin',
382
+ };
383
+ const { model } = setup('/admin/settings/public-forms/form-1/view/popup', nestedLayout);
384
+
385
+ await waitFor(() => {
386
+ expect(model.currentLayoutRoute).toMatchObject({
387
+ type: 'page',
388
+ basePathname: '/admin/settings/public-forms',
389
+ pageUid: 'form-1',
390
+ viewStack: [{ viewUid: 'form-1' }, { viewUid: 'popup' }],
391
+ });
392
+ });
393
+ });
394
+
395
+ it('parses empty nested layout route by matched layout pathname', async () => {
396
+ const nestedLayout: LayoutDefinition = {
397
+ ...layout,
398
+ routeName: 'admin.settings.publicForms.layout',
399
+ routePath: '',
400
+ rootRouteName: 'admin',
401
+ };
402
+ const { model } = setup('/admin/settings/public-forms/form-1/view/popup', nestedLayout);
403
+
404
+ await waitFor(() => {
405
+ expect(model.currentLayoutRoute).toMatchObject({
406
+ type: 'page',
407
+ basePathname: '/admin/settings/public-forms',
408
+ pageUid: 'form-1',
409
+ viewStack: [{ viewUid: 'form-1' }, { viewUid: 'popup' }],
410
+ });
411
+ });
412
+ });
413
+
414
+ it('clears local layout route when the content route unmounts', async () => {
415
+ const { model, router } = setup('/test/page-1/view/popup');
416
+
417
+ await waitFor(() => {
418
+ expect(model.currentLayoutRoute).toMatchObject({
419
+ type: 'page',
420
+ pageUid: 'page-1',
421
+ });
422
+ });
423
+
424
+ await router.navigate('/test');
425
+
426
+ await waitFor(() => {
427
+ expect(model.currentLayoutRoute).toBeNull();
428
+ });
429
+ });
430
+
431
+ it('does not clear local layout route while switching within the same page view stack', async () => {
432
+ const clearEvents: Array<{ routePathname?: string; before: string | null; after: string | null }> = [];
433
+
434
+ class TrackingLayoutModel extends TestLayoutModel {
435
+ clearLayoutRoute(routeLike?: Parameters<BaseLayoutModel['clearLayoutRoute']>[0]) {
436
+ const event: { routePathname?: string; before: string | null; after: string | null } = {
437
+ routePathname: routeLike?.pathname,
438
+ before: this.currentLayoutRoute?.pathname || null,
439
+ after: null,
440
+ };
441
+ super.clearLayoutRoute(routeLike);
442
+ event.after = this.currentLayoutRoute?.pathname || null;
443
+ clearEvents.push(event);
444
+ }
445
+ }
446
+
447
+ const { model, router } = setup('/test/page-1', layout, TrackingLayoutModel);
448
+
449
+ await waitFor(() => {
450
+ expect(model.currentLayoutRoute).toMatchObject({
451
+ type: 'page',
452
+ pageUid: 'page-1',
453
+ pathname: '/test/page-1',
454
+ });
455
+ });
456
+
457
+ await router.navigate('/test/page-1/view/popup');
458
+
459
+ await waitFor(() => {
460
+ expect(model.currentLayoutRoute).toMatchObject({
461
+ type: 'page',
462
+ pageUid: 'page-1',
463
+ pathname: '/test/page-1/view/popup',
464
+ });
465
+ });
466
+
467
+ expect(clearEvents).not.toContainEqual({
468
+ routePathname: '/test/page-1',
469
+ before: '/test/page-1',
470
+ after: null,
471
+ });
472
+ });
473
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export * from './LayoutManager';
11
+ export * from './LayoutContentRoute';
12
+ export * from './LayoutRoute';
13
+ export * from './types';
14
+ export * from './utils';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export interface LayoutRegisterOptions {
11
+ routeName: string;
12
+ routePath: string;
13
+ uid: string;
14
+ layoutModelClass: string;
15
+ rootPageModelClass?: string;
16
+ childPageModelClass?: string;
17
+ authCheck?: boolean;
18
+ }
19
+
20
+ export interface LayoutDefinition extends Required<LayoutRegisterOptions> {
21
+ rootRouteName: string;
22
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export function getLayoutPageRouteName(routeName: string) {
11
+ return `${routeName}.__page`;
12
+ }
13
+
14
+ export function getLayoutPageTabRouteName(routeName: string) {
15
+ return `${routeName}.__pageTab`;
16
+ }
17
+
18
+ export function getLayoutPageViewRouteName(routeName: string) {
19
+ return `${routeName}.__pageView`;
20
+ }
21
+
22
+ export function getLayoutPageTabViewRouteName(routeName: string) {
23
+ return `${routeName}.__pageTabView`;
24
+ }
25
+
26
+ export function getLayoutContentRouteNames(routeName: string) {
27
+ return [
28
+ getLayoutPageRouteName(routeName),
29
+ getLayoutPageTabRouteName(routeName),
30
+ getLayoutPageViewRouteName(routeName),
31
+ getLayoutPageTabViewRouteName(routeName),
32
+ ];
33
+ }
34
+
35
+ export function isLayoutContentRouteName(routeName: string, targetRouteName?: string) {
36
+ return !!targetRouteName && getLayoutContentRouteNames(routeName).includes(targetRouteName);
37
+ }