@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,264 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import type { CollectionOptions } from '@nocobase/flow-engine';
11
+ import { FlowEngineProvider } from '@nocobase/flow-engine';
12
+ import { act, render } from '@testing-library/react';
13
+ import React, { useState } from 'react';
14
+ import { describe, expect, it } from 'vitest';
15
+ import { createMockClient } from '../../MockApplication';
16
+ import { ExtendCollectionsProvider } from '../ExtendCollectionsProvider';
17
+
18
+ // `MockApplication` isn't exported as a named type, so infer from the factory.
19
+ type AppInstance = ReturnType<typeof createMockClient>;
20
+
21
+ function makeApp(): AppInstance {
22
+ const app = createMockClient();
23
+ // The mock client wires a lazy `appInfo` getter to `GET app:getInfo`; any
24
+ // proxy iteration of the FlowEngineContext can trip it. Pre-stub so a 404
25
+ // doesn't surface as an unhandled axios rejection during the test run.
26
+ app.apiMock.onGet('app:getInfo').reply(200, { data: { version: 'test' } });
27
+ return app;
28
+ }
29
+
30
+ const LOCKED: CollectionOptions = {
31
+ name: 'lockedUsers',
32
+ fields: [{ name: 'id', type: 'integer', interface: 'integer' }],
33
+ };
34
+
35
+ const USERS: CollectionOptions = {
36
+ name: 'users',
37
+ fields: [{ name: 'username', type: 'string', interface: 'input' }],
38
+ };
39
+
40
+ const POSTS: CollectionOptions = {
41
+ name: 'posts',
42
+ fields: [{ name: 'title', type: 'string', interface: 'input' }],
43
+ };
44
+
45
+ function mountWith(app: AppInstance, node: React.ReactNode) {
46
+ return render(<FlowEngineProvider engine={app.flowEngine}>{node}</FlowEngineProvider>);
47
+ }
48
+
49
+ function getMain(app: AppInstance) {
50
+ return app.dataSourceManager.getDataSource('main');
51
+ }
52
+
53
+ describe('ExtendCollectionsProvider', () => {
54
+ // The defining contract: a page-inner that reads `getCollection(name)` in its
55
+ // own render body (the LockedUsersPage pattern at LockedUsersPage.tsx:156-157)
56
+ // must see the registered collection on its FIRST render, with no extra tick.
57
+ it('lets children read the registered collection during their first render', () => {
58
+ const app = makeApp();
59
+ let firstRenderResult: { name?: string } | undefined;
60
+
61
+ const Child: React.FC = () => {
62
+ // Read synchronously during render — this is the contract that forces
63
+ // the provider to register collections in render-phase rather than in
64
+ // an effect.
65
+ const found = getMain(app)?.getCollection?.(LOCKED.name);
66
+ firstRenderResult = found ? { name: found.name } : undefined;
67
+ return <div>child</div>;
68
+ };
69
+
70
+ mountWith(
71
+ app,
72
+ <ExtendCollectionsProvider collections={[LOCKED]}>
73
+ <Child />
74
+ </ExtendCollectionsProvider>,
75
+ );
76
+
77
+ expect(firstRenderResult).toEqual({ name: LOCKED.name });
78
+ });
79
+
80
+ it('removes only the collections it added when unmounted', () => {
81
+ const app = makeApp();
82
+ // Pre-existing collection in the data source — provider must not touch it.
83
+ getMain(app).addCollection(USERS);
84
+
85
+ const { unmount } = mountWith(
86
+ app,
87
+ <ExtendCollectionsProvider collections={[LOCKED]}>
88
+ <span>inside</span>
89
+ </ExtendCollectionsProvider>,
90
+ );
91
+
92
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
93
+ expect(getMain(app).getCollection(USERS.name)?.name).toBe(USERS.name);
94
+
95
+ unmount();
96
+
97
+ expect(getMain(app).getCollection(LOCKED.name)).toBeUndefined();
98
+ // The provider didn't add USERS, so it must leave it alone.
99
+ expect(getMain(app).getCollection(USERS.name)?.name).toBe(USERS.name);
100
+ });
101
+
102
+ // Direct regression for the lazy-ref idempotency: even when the provider
103
+ // re-renders many times (which is what StrictMode dev's double-render and
104
+ // any caller-driven re-render look like to the provider), it must not
105
+ // re-register the same collection or accumulate duplicate ownership.
106
+ it('only calls addCollection once across many re-renders of the same mount', () => {
107
+ const app = makeApp();
108
+ let addCount = 0;
109
+ const origAdd = getMain(app).addCollection.bind(getMain(app));
110
+ getMain(app).addCollection = (c: CollectionOptions) => {
111
+ addCount += 1;
112
+ origAdd(c);
113
+ };
114
+
115
+ const Host: React.FC = () => {
116
+ const [, setTick] = useState(0);
117
+ // Force a render burst on mount — covers StrictMode dev's second render
118
+ // and any other parent-driven re-renders. The lazy-ref guard in the
119
+ // provider must hold across all of them.
120
+ React.useEffect(() => {
121
+ setTick((n) => n + 1);
122
+ setTick((n) => n + 1);
123
+ }, []);
124
+ return (
125
+ <ExtendCollectionsProvider collections={[LOCKED]}>
126
+ <span>inside</span>
127
+ </ExtendCollectionsProvider>
128
+ );
129
+ };
130
+
131
+ const { unmount } = mountWith(app, <Host />);
132
+
133
+ expect(addCount).toBe(1);
134
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
135
+
136
+ unmount();
137
+ expect(getMain(app).getCollection(LOCKED.name)).toBeUndefined();
138
+ });
139
+
140
+ // Default semantics: "static-at-mount". Caller's prop changes are ignored.
141
+ describe('with syncOnChange={false} (default)', () => {
142
+ it('ignores a `collections` prop reference change after mount', () => {
143
+ const app = makeApp();
144
+
145
+ const Host: React.FC = () => {
146
+ const [list, setList] = useState<CollectionOptions[]>([LOCKED]);
147
+ return (
148
+ <>
149
+ <button type="button" onClick={() => setList([LOCKED, POSTS])}>
150
+ add posts
151
+ </button>
152
+ <ExtendCollectionsProvider collections={list}>
153
+ <span>inside</span>
154
+ </ExtendCollectionsProvider>
155
+ </>
156
+ );
157
+ };
158
+
159
+ const { getByText } = mountWith(app, <Host />);
160
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
161
+ expect(getMain(app).getCollection(POSTS.name)).toBeUndefined();
162
+
163
+ act(() => {
164
+ getByText('add posts').click();
165
+ });
166
+
167
+ // POSTS must NOT have been registered — syncOnChange is off.
168
+ expect(getMain(app).getCollection(POSTS.name)).toBeUndefined();
169
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
170
+ });
171
+ });
172
+
173
+ describe('with syncOnChange={true}', () => {
174
+ it('adds collections newly added to the prop and removes ones dropped from it', () => {
175
+ const app = makeApp();
176
+
177
+ const Host: React.FC = () => {
178
+ const [list, setList] = useState<CollectionOptions[]>([LOCKED, POSTS]);
179
+ return (
180
+ <>
181
+ <button type="button" onClick={() => setList([POSTS, USERS])}>
182
+ swap
183
+ </button>
184
+ <ExtendCollectionsProvider collections={list} syncOnChange>
185
+ <span>inside</span>
186
+ </ExtendCollectionsProvider>
187
+ </>
188
+ );
189
+ };
190
+
191
+ const { getByText } = mountWith(app, <Host />);
192
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
193
+ expect(getMain(app).getCollection(POSTS.name)?.name).toBe(POSTS.name);
194
+ expect(getMain(app).getCollection(USERS.name)).toBeUndefined();
195
+
196
+ act(() => {
197
+ getByText('swap').click();
198
+ });
199
+
200
+ // LOCKED was dropped from the prop → removed.
201
+ expect(getMain(app).getCollection(LOCKED.name)).toBeUndefined();
202
+ // POSTS was in both lists → kept.
203
+ expect(getMain(app).getCollection(POSTS.name)?.name).toBe(POSTS.name);
204
+ // USERS is new → added.
205
+ expect(getMain(app).getCollection(USERS.name)?.name).toBe(USERS.name);
206
+ });
207
+
208
+ it('still leaves pre-existing collections it never owned alone after diff', () => {
209
+ const app = makeApp();
210
+ // Pre-existing in the data source before the provider mounts.
211
+ getMain(app).addCollection(USERS);
212
+
213
+ const Host: React.FC = () => {
214
+ const [list, setList] = useState<CollectionOptions[]>([LOCKED, USERS]);
215
+ return (
216
+ <>
217
+ <button type="button" onClick={() => setList([LOCKED])}>
218
+ drop users
219
+ </button>
220
+ <ExtendCollectionsProvider collections={list} syncOnChange>
221
+ <span>inside</span>
222
+ </ExtendCollectionsProvider>
223
+ </>
224
+ );
225
+ };
226
+
227
+ const { getByText } = mountWith(app, <Host />);
228
+ // USERS was already there, so the provider never owned it.
229
+ expect(getMain(app).getCollection(USERS.name)?.name).toBe(USERS.name);
230
+
231
+ act(() => {
232
+ getByText('drop users').click();
233
+ });
234
+
235
+ // The prop dropped USERS, but the provider doesn't own it — must not
236
+ // accidentally remove it.
237
+ expect(getMain(app).getCollection(USERS.name)?.name).toBe(USERS.name);
238
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
239
+ });
240
+ });
241
+
242
+ // Mid-session reload: the data source manager wipes everything and reloads
243
+ // from the server; client-only entries this provider registered would be
244
+ // gone. The `dataSource:loaded` event handler re-adds owned entries.
245
+ it('re-registers owned collections after a dataSource:loaded event', () => {
246
+ const app = makeApp();
247
+
248
+ mountWith(
249
+ app,
250
+ <ExtendCollectionsProvider collections={[LOCKED]}>
251
+ <span>inside</span>
252
+ </ExtendCollectionsProvider>,
253
+ );
254
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
255
+
256
+ // Simulate the data source manager wiping then re-broadcasting the event.
257
+ act(() => {
258
+ getMain(app).removeCollection(LOCKED.name);
259
+ app.eventBus.dispatchEvent(new CustomEvent('dataSource:loaded', { detail: { dataSourceKey: 'main' } }));
260
+ });
261
+
262
+ expect(getMain(app).getCollection(LOCKED.name)?.name).toBe(LOCKED.name);
263
+ });
264
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ export * from './ExtendCollectionsProvider';
@@ -7,15 +7,33 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { FlowModelRenderer, useFlowEngine, useFlowModelById, useFlowViewContext } from '@nocobase/flow-engine';
11
- import type { FlowModel } from '@nocobase/flow-engine';
10
+ import {
11
+ FlowModelRenderer,
12
+ isInheritedFrom,
13
+ useFlowEngine,
14
+ useFlowModelById,
15
+ useFlowViewContext,
16
+ } from '@nocobase/flow-engine';
17
+ import type { FlowModel, FlowModelRendererProps, ModelConstructor } from '@nocobase/flow-engine';
12
18
  import { useRequest } from 'ahooks';
13
19
  import React from 'react';
14
20
  import FlowRoute from './components/FlowRoute';
15
21
  import { SkeletonFallback } from './components/SkeletonFallback';
22
+ import { ChildPageModel } from './models';
16
23
 
17
- function InternalFlowPage({ uid, ...props }) {
24
+ const defaultPageFlowSettings: FlowModelRendererProps['showFlowSettings'] = {
25
+ showBackground: false,
26
+ showBorder: false,
27
+ };
28
+
29
+ function InternalFlowPage({
30
+ uid,
31
+ showFlowSettings,
32
+ ...props
33
+ }: { uid: string; showFlowSettings?: FlowModelRendererProps['showFlowSettings'] } & Record<string, unknown>) {
18
34
  const model = useFlowModelById(uid);
35
+ const resolvedShowFlowSettings = showFlowSettings ?? model?.props?.showFlowSettings ?? defaultPageFlowSettings;
36
+
19
37
  return (
20
38
  <FlowModelRenderer
21
39
  model={model}
@@ -25,7 +43,7 @@ function InternalFlowPage({ uid, ...props }) {
25
43
  />
26
44
  }
27
45
  hideRemoveInSettings
28
- showFlowSettings={{ showBackground: false, showBorder: false }}
46
+ showFlowSettings={resolvedShowFlowSettings}
29
47
  {...props}
30
48
  />
31
49
  );
@@ -36,14 +54,21 @@ type FlowPageProps = {
36
54
  parentId?: string;
37
55
  onModelLoaded?: (uid: string, model: FlowModel) => void;
38
56
  defaultTabTitle?: string;
57
+ showFlowSettings?: FlowModelRendererProps['showFlowSettings'];
39
58
  };
40
59
 
41
60
  export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknown>) => {
42
61
  const { pageModelClass = 'ChildPageModel', parentId, onModelLoaded, defaultTabTitle, ...rest } = props;
43
62
  const flowEngine = useFlowEngine();
44
63
  const ctx = useFlowViewContext();
45
- const { loading, data } = useRequest(
64
+ const { loading, data, error } = useRequest(
46
65
  async () => {
66
+ const ModelClass = await flowEngine.getModelClassAsync(pageModelClass);
67
+ if (!ModelClass) {
68
+ throw new Error(`[NocoBase] Page model class '${pageModelClass}' is not registered.`);
69
+ }
70
+ const shouldInjectDefaultChildTab =
71
+ ModelClass === ChildPageModel || isInheritedFrom(ModelClass as ModelConstructor, ChildPageModel);
47
72
  const options = {
48
73
  async: true,
49
74
  parentId,
@@ -51,7 +76,7 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
51
76
  subType: 'object',
52
77
  use: pageModelClass,
53
78
  };
54
- if (pageModelClass === 'ChildPageModel') {
79
+ if (shouldInjectDefaultChildTab) {
55
80
  const tabTitle = defaultTabTitle || flowEngine.translate?.('Details');
56
81
  options['subModels'] = {
57
82
  tabs: [
@@ -86,9 +111,12 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
86
111
  return data;
87
112
  },
88
113
  {
89
- refreshDeps: [parentId],
114
+ refreshDeps: [parentId, pageModelClass, defaultTabTitle],
90
115
  },
91
116
  );
117
+ if (error) {
118
+ throw error;
119
+ }
92
120
  if (loading || !data?.uid) {
93
121
  return <SkeletonFallback style={{ margin: ctx?.isMobileLayout ? 8 : ctx?.themeToken.marginBlock }} />;
94
122
  }
@@ -0,0 +1,79 @@
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 { render } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { afterEach, describe, expect, it, vi } from 'vitest';
13
+ import { FlowPage } from '../FlowPage';
14
+
15
+ const mocks = vi.hoisted(() => ({
16
+ rendererProps: undefined as any,
17
+ model: {
18
+ uid: 'public-form-page',
19
+ props: {
20
+ showFlowSettings: false,
21
+ },
22
+ context: {
23
+ isMobileLayout: false,
24
+ themeToken: {
25
+ marginBlock: 16,
26
+ },
27
+ },
28
+ },
29
+ }));
30
+
31
+ vi.mock('@nocobase/flow-engine', async () => {
32
+ const actual = await vi.importActual<any>('@nocobase/flow-engine');
33
+ const ReactModule = await import('react');
34
+
35
+ return {
36
+ ...actual,
37
+ FlowModelRenderer: vi.fn((props: any) => {
38
+ mocks.rendererProps = props;
39
+ return ReactModule.createElement('div', { 'data-testid': 'flow-model-renderer' });
40
+ }),
41
+ useFlowEngine: vi.fn(() => ({
42
+ getModelClassAsync: vi.fn(),
43
+ loadOrCreateModel: vi.fn(),
44
+ context: {},
45
+ })),
46
+ useFlowModelById: vi.fn(() => mocks.model),
47
+ useFlowViewContext: vi.fn(() => ({
48
+ themeToken: {
49
+ marginBlock: 16,
50
+ },
51
+ })),
52
+ };
53
+ });
54
+
55
+ vi.mock('ahooks', async () => {
56
+ const actual = await vi.importActual<any>('ahooks');
57
+ return {
58
+ ...actual,
59
+ useRequest: vi.fn(() => ({
60
+ loading: false,
61
+ data: {
62
+ uid: 'public-form-page',
63
+ },
64
+ error: null,
65
+ })),
66
+ };
67
+ });
68
+
69
+ describe('FlowPage', () => {
70
+ afterEach(() => {
71
+ mocks.rendererProps = undefined;
72
+ });
73
+
74
+ it('uses showFlowSettings from page model props', () => {
75
+ render(<FlowPage />);
76
+
77
+ expect(mocks.rendererProps?.showFlowSettings).toBe(false);
78
+ });
79
+ });