@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,131 @@
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 { RouteContext } from '@ant-design/pro-layout';
11
+ import _ from 'lodash';
12
+ import React, { createContext, FC, memo, useContext, useRef } from 'react';
13
+ import {
14
+ UNSAFE_DataRouterContext,
15
+ UNSAFE_DataRouterStateContext,
16
+ UNSAFE_LocationContext,
17
+ UNSAFE_RouteContext,
18
+ } from 'react-router-dom';
19
+
20
+ const KeepAliveContext = createContext(true);
21
+ const hidden = { display: 'none' };
22
+
23
+ export const KeepAliveProvider: FC<{ active: boolean; parentActive: boolean }> = memo(
24
+ ({ children, active, parentActive }) => {
25
+ const currentLocationContext = useContext(UNSAFE_LocationContext);
26
+ const currentRouteContext = useContext(UNSAFE_RouteContext);
27
+ const currentDataRouterContext = useContext(UNSAFE_DataRouterContext);
28
+ const currentDataRouterStateContext = useContext(UNSAFE_DataRouterStateContext);
29
+ const routeContextValue = useContext(RouteContext);
30
+
31
+ const prevLocationContextRef = useRef(currentLocationContext);
32
+ const prevRouteContextRef = useRef(currentRouteContext);
33
+ const prevDataRouterContextRef = useRef(currentDataRouterContext);
34
+ const prevDataRouterStateContextRef = useRef(currentDataRouterStateContext);
35
+ const prevRouteContextValueRef = useRef(routeContextValue);
36
+
37
+ if (active) {
38
+ prevDataRouterContextRef.current = currentDataRouterContext;
39
+ prevDataRouterStateContextRef.current = currentDataRouterStateContext;
40
+ prevRouteContextValueRef.current = routeContextValue;
41
+ }
42
+
43
+ if (
44
+ active &&
45
+ !_.isEqual(_.omit(prevLocationContextRef.current.location, 'key'), _.omit(currentLocationContext.location, 'key'))
46
+ ) {
47
+ prevLocationContextRef.current = currentLocationContext;
48
+ }
49
+
50
+ if (active && !_.isEqual(prevRouteContextRef.current, currentRouteContext)) {
51
+ prevRouteContextRef.current = currentRouteContext;
52
+ }
53
+
54
+ return (
55
+ <div style={active ? { height: '100%' } : hidden}>
56
+ <RouteContext.Provider value={prevRouteContextValueRef.current}>
57
+ <UNSAFE_DataRouterContext.Provider value={prevDataRouterContextRef.current}>
58
+ <UNSAFE_DataRouterStateContext.Provider value={prevDataRouterStateContextRef.current}>
59
+ <UNSAFE_LocationContext.Provider value={prevLocationContextRef.current}>
60
+ <UNSAFE_RouteContext.Provider value={prevRouteContextRef.current}>
61
+ <KeepAliveContext.Provider value={parentActive === false ? false : active}>
62
+ {children}
63
+ </KeepAliveContext.Provider>
64
+ </UNSAFE_RouteContext.Provider>
65
+ </UNSAFE_LocationContext.Provider>
66
+ </UNSAFE_DataRouterStateContext.Provider>
67
+ </UNSAFE_DataRouterContext.Provider>
68
+ </RouteContext.Provider>
69
+ </div>
70
+ );
71
+ },
72
+ );
73
+
74
+ export const useKeepAlive = () => {
75
+ const active = useContext(KeepAliveContext);
76
+ return { active };
77
+ };
78
+
79
+ interface KeepAliveProps {
80
+ uid: string;
81
+ children: (uid: string) => React.ReactNode;
82
+ }
83
+
84
+ const MINIMUM_CACHED_PAGES = 5;
85
+ const MAXIMUM_CACHED_PAGES = 15;
86
+
87
+ const getMaxPageCount = () => {
88
+ const baseCount = MINIMUM_CACHED_PAGES;
89
+
90
+ try {
91
+ const memory = (navigator as any).deviceMemory;
92
+ if (memory) {
93
+ return Math.min(Math.max(baseCount, memory * 3), MAXIMUM_CACHED_PAGES);
94
+ }
95
+
96
+ const cores = navigator.hardwareConcurrency;
97
+ if (cores) {
98
+ return cores >= 8 ? MAXIMUM_CACHED_PAGES : cores >= 4 ? 7 : baseCount;
99
+ }
100
+
101
+ return baseCount;
102
+ } catch (e) {
103
+ return baseCount;
104
+ }
105
+ };
106
+
107
+ const MAX_RENDERED_PAGE_COUNT = getMaxPageCount();
108
+
109
+ export const KeepAlive: FC<KeepAliveProps> = React.memo(({ children, uid }) => {
110
+ const { active } = useKeepAlive();
111
+ const renderedUidListRef = useRef<string[]>([]);
112
+
113
+ if (!renderedUidListRef.current.includes(uid)) {
114
+ renderedUidListRef.current.push(uid);
115
+ if (renderedUidListRef.current.length > MAX_RENDERED_PAGE_COUNT) {
116
+ renderedUidListRef.current = renderedUidListRef.current.slice(-MAX_RENDERED_PAGE_COUNT);
117
+ }
118
+ }
119
+
120
+ return (
121
+ <>
122
+ {renderedUidListRef.current.map((renderedUid) => (
123
+ <KeepAliveProvider active={renderedUid === uid} key={renderedUid} parentActive={active}>
124
+ {children(renderedUid)}
125
+ </KeepAliveProvider>
126
+ ))}
127
+ </>
128
+ );
129
+ });
130
+
131
+ KeepAlive.displayName = 'KeepAlive';
@@ -14,9 +14,9 @@ Grouped by purpose: form containers, form fields, data table, utilities.
14
14
 
15
15
  #### DrawerFormLayout
16
16
 
17
- Drawer-style form layout. Pair with `ctx.viewer.drawer({ content })`.
17
+ Drawer-style form layout. Pair with `ctx.viewer.drawer({ closable: true, content })`.
18
18
 
19
- - Top: a close icon next to the title. Clicking close fires `onCancel` and dismisses the drawer
19
+ - Top: title only; the native close X is rendered by antd Drawer you must pass `closable: true` on the `viewer.drawer` call for it to appear
20
20
  - Bottom: default Cancel / Submit buttons; override the whole footer with `footer`
21
21
  - Middle: caller-supplied `<Form>` instance + fields
22
22
 
@@ -25,6 +25,7 @@ import { DrawerFormLayout } from '@nocobase/client-v2';
25
25
 
26
26
  ctx.viewer.drawer({
27
27
  width: '50%',
28
+ closable: true, // restore antd Drawer's native close X
28
29
  content: () => (
29
30
  <DrawerFormLayout
30
31
  title={t('Add authenticator')}
@@ -41,17 +42,19 @@ ctx.viewer.drawer({
41
42
 
42
43
  Key props:
43
44
 
44
- - `title`: title node (rendered next to the close icon)
45
- - `onCancel` / `onSubmit`: callbacks; the drawer closes automatically once they resolve. Throw from `onSubmit` to keep the drawer open (e.g. on a validation error)
45
+ - `title`: title node
46
+ - `onSubmit`: callback; the drawer closes automatically once it resolves. Throw to keep the drawer open (e.g. on a validation error)
46
47
  - `submitting`: drives the Submit button's loading state
47
48
  - `submitText` / `cancelText`: button labels
48
49
  - `footer`: full override of the footer content (replaces the default Cancel + Submit pair)
49
50
 
51
+ To intercept close (e.g. dirty-form confirmation), use the lower-level `viewer.drawer({ preventClose, beforeClose })` hooks — this layout no longer wraps a custom cancel handler.
52
+
50
53
  #### DialogFormLayout
51
54
 
52
55
  Dialog-style form layout, the centered counterpart of `DrawerFormLayout`. Pair with `ctx.viewer.dialog({ closable: true, content })`.
53
56
 
54
- The only visual difference from the drawer version: the title is a bare string (no inline close icon), relying on antd Modal's native top-right X. Note that `viewer.dialog` disables antd's native X by default you have to pass `closable: true` explicitly for it to appear.
57
+ The only visual difference from the drawer version is where the native close X sits antd Drawer renders it at the top-left of the title bar, antd Modal at the top-right. Both layouts rely on the caller passing `closable: true` at the viewer call site; neither renders a close icon itself.
55
58
 
56
59
  ```tsx
57
60
  import { DialogFormLayout } from '@nocobase/client-v2';
@@ -73,7 +76,7 @@ When to pick which:
73
76
  - **Drawer**: long forms with lots of fields that benefit from a full-height side panel (settings-page "Add / Edit")
74
77
  - **Dialog**: short forms that ask for quick confirmation (bind, change password, two-factor verify)
75
78
 
76
- Props are identical to `DrawerFormLayout` they're drop-in replacements at the API level.
79
+ Props are nearly identical to `DrawerFormLayout`, with one extra: `DialogFormLayout` accepts an `onCancel` callback (fired by both the Cancel button and the native X) for "discard changes" confirmations.
77
80
 
78
81
  ### Form fields
79
82
 
@@ -161,6 +164,7 @@ Key props:
161
164
  - `namespaces`: restrict the picker to specific top-level namespaces. Omit to expose every registered top-level property
162
165
  - `extraNodes`: static leaves appended after the namespace-filtered nodes. Use for variables that only make sense in the current page (e.g. `$resetLink`)
163
166
  - `converters`: override the default path ↔ string converters. `EnvVariableInput` uses this hook to lock its output to `$env`
167
+ - `delimiters`: token pair wrapping the stored variable reference. Defaults to `['{{', '}}']` (Handlebars HTML-escaped). Pass `['{{{', '}}}']` for fields rendered as HTML where escaping would corrupt the variable value — e.g. the in-app message body
164
168
  - `value` / `onChange` / `placeholder` / `disabled`: standard controlled-input props
165
169
 
166
170
  Under the hood `VariableInput` wraps `VariableHybridInput` (inline pills), `VariableTextArea` wraps `TextAreaWithContextSelector` (textarea + variable button). Both share the same MetaTree.
@@ -272,6 +276,48 @@ Companion exports:
272
276
  - `PAGE_SIZE_OPTIONS`: suggested page-size dropdown values `[5, 10, 20, 50, 100, 200]`
273
277
  - `SortHandle`: standalone handle component, exported from `@nocobase/client-v2` for embedding into custom columns
274
278
 
279
+ ### Filter
280
+
281
+ #### CollectionFilter
282
+
283
+ Filter button bound to a Collection. Clicking opens a Popover hosting a multi-condition filter form (field picker + operator + value control). Submit dismisses the Popover and emits the compiled NocoBase filter via `onChange`; Reset keeps the Popover open and emits `undefined`.
284
+
285
+ ```tsx
286
+ import { CollectionFilter, ExtendCollectionsProvider } from '@nocobase/client-v2';
287
+ import lockedUsersCollection from '../../collections/locked-users';
288
+
289
+ function Page() {
290
+ const main = engine.context.dataSourceManager?.getDataSource?.('main');
291
+ const collection = main?.getCollection?.(lockedUsersCollection.name);
292
+
293
+ const listRequest = useRequest(
294
+ async (filter) => api.resource('lockedUsers').list({ ...(filter ? { filter } : {}) }),
295
+ { defaultParams: [undefined] },
296
+ );
297
+
298
+ return (
299
+ <ExtendCollectionsProvider collections={[lockedUsersCollection]}>
300
+ <CollectionFilter collection={collection} onChange={listRequest.run} t={t} />
301
+ {/* table … */}
302
+ </ExtendCollectionsProvider>
303
+ );
304
+ }
305
+ ```
306
+
307
+ Key props:
308
+
309
+ - `collection`: the Collection that drives the field picker. The button is disabled while it's `undefined`
310
+ - `onChange: (filter) => void`: fired on Submit and Reset with the compiled NocoBase filter (`undefined` on Reset). Most pages forward straight to `listRequest.run`
311
+ - `t`: translator. Pass `useT()` from a plugin's `locale.ts` so server-side `{{t("…")}}` macros in field / operator labels get expanded — plain react-i18next's `t` leaves them as literal template strings
312
+ - `filterableFieldNames`: whitelist of root-level field names to expose
313
+ - `noIgnore`: bypass the whitelist
314
+ - `buttonText`: override the trigger label; defaults to `t('Filter')`
315
+ - `showCount`: show the `(N)` condition-count badge on the trigger; defaults to `true`
316
+ - `popoverProps` / `buttonProps`: pass-through to the antd `Popover` / `Button`
317
+ - `popoverMinWidth`: min-width of the popover body; defaults to `520`
318
+
319
+ If the target Collection is `schema-only` (not auto-published from the server to the v2 data source), wrap the page in `<ExtendCollectionsProvider>` so `CollectionFilter` can resolve it by name.
320
+
275
321
  ### Utilities
276
322
 
277
323
  #### createFormRegistry
@@ -302,6 +348,44 @@ Use this when a plugin needs an extension point for "same name + same shape + di
302
348
 
303
349
  Re-registering the same `name` overwrites the previous entry and emits a `console.warn` — HMR doesn't throw, and unintended duplicates surface in dev.
304
350
 
351
+ ## data-source/
352
+
353
+ Components that wire collections / data sources into the React tree. Exported from the top level of `@nocobase/client-v2`.
354
+
355
+ ### ExtendCollectionsProvider
356
+
357
+ Mount-scoped collection injector. On mount it registers the given collections into the target data source; on unmount it removes them. A `dataSource:loaded` listener re-applies the registration so mid-session reloads don't wipe injected collections.
358
+
359
+ ```tsx
360
+ import { ExtendCollectionsProvider } from '@nocobase/client-v2';
361
+ import lockedUsersCollection from '../../collections/locked-users';
362
+
363
+ // Module-level constant — keeps the reference stable so the provider's
364
+ // effect doesn't re-run on every parent re-render.
365
+ const collections = [lockedUsersCollection];
366
+
367
+ export function LockedUsersPage() {
368
+ return (
369
+ <ExtendCollectionsProvider collections={collections}>
370
+ <LockedUsersPageInner />
371
+ </ExtendCollectionsProvider>
372
+ );
373
+ }
374
+ ```
375
+
376
+ Key props:
377
+
378
+ - `collections: CollectionOptions[]`: collections to inject. The provider only adds names that aren't already present, and on unmount removes only the ones it added
379
+ - `dataSource`: target data source key; defaults to `'main'`
380
+ - `children`: subtree covered by the injection
381
+
382
+ When to use:
383
+
384
+ - The server-side collection is `schema-only` and doesn't get auto-published to the client data source (e.g. `lockedUsers`)
385
+ - You need a client-side mirror that should be visible only inside the current page, not registered globally
386
+
387
+ Typical pairing: use together with `<CollectionFilter>` — the provider makes the collection resolvable; the filter button consumes it.
388
+
305
389
  ## When to add a new component here
306
390
 
307
391
  - Two or more plugins need the same field or container shape — promote it to this folder
@@ -14,9 +14,9 @@
14
14
 
15
15
  #### DrawerFormLayout
16
16
 
17
- 抽屉形态的表单 layout。配合 `ctx.viewer.drawer({ content })` 用。
17
+ 抽屉形态的表单 layout。配合 `ctx.viewer.drawer({ closable: true, content })` 用。
18
18
 
19
- - 顶部 Header:左上角一个 close 图标 + 标题。点击 close 会触发 `onCancel` 然后关闭抽屉
19
+ - 顶部 Header:只放标题;左侧的关闭 X 来自 antd Drawer——必须在 `viewer.drawer` 上显式传 `closable: true` 才会出现
20
20
  - 底部 Footer:默认 Cancel / Submit 两个按钮;可以用 `footer` 完全替换
21
21
  - 中间 children:调用方自己放 `<Form>` 实例 + 字段
22
22
 
@@ -25,6 +25,7 @@ import { DrawerFormLayout } from '@nocobase/client-v2';
25
25
 
26
26
  ctx.viewer.drawer({
27
27
  width: '50%',
28
+ closable: true, // 关键:开启 antd Drawer 原生关闭 X
28
29
  content: () => (
29
30
  <DrawerFormLayout
30
31
  title={t('添加认证器')}
@@ -41,17 +42,19 @@ ctx.viewer.drawer({
41
42
 
42
43
  主要属性:
43
44
 
44
- - `title`:标题节点(旁边带 close 图标)
45
- - `onCancel` / `onSubmit`:回调,resolve 后会自动关闭抽屉。Submit 里 throw 可以让抽屉保持打开(比如校验失败)
45
+ - `title`:标题节点
46
+ - `onSubmit`:回调,resolve 后会自动关闭抽屉。throw 可以让抽屉保持打开(比如校验失败)
46
47
  - `submitting`:驱动 Submit 按钮的 loading
47
48
  - `submitText` / `cancelText`:按钮文字
48
49
  - `footer`:完全自定义 Footer 内容(覆盖默认两个按钮)
49
50
 
51
+ 需要在关闭前做「未保存改动」之类的确认,用更底层的 `viewer.drawer({ preventClose, beforeClose })`,这层 layout 不再包装 cancel 拦截。
52
+
50
53
  #### DialogFormLayout
51
54
 
52
- 弹窗形态的表单 layout,跟 `DrawerFormLayout` 是同源对偶。配合 `ctx.viewer.dialog({ closable: true, content })` 用。
55
+ 弹窗形态的表单 layout,跟 `DrawerFormLayout` 同形。配合 `ctx.viewer.dialog({ closable: true, content })` 用。
53
56
 
54
- Drawer 版本的差异只有一点:title 是裸字符串(不带 close 图标),依赖 antd Modal 自带的右上角 X。注意 `viewer.dialog` 默认会禁用 antd 的原生 X——必须显式传 `closable: true` 才会出现。
57
+ 视觉上的差异只有关闭 X 的位置——Drawer antd Drawer 自带的左上角 X,Dialog 是 antd Modal 自带的右上角 X。两边都依赖在 viewer 调用处显式传 `closable: true`,layout 自己都不渲染 close 图标。
55
58
 
56
59
  ```tsx
57
60
  import { DialogFormLayout } from '@nocobase/client-v2';
@@ -73,7 +76,7 @@ ctx.viewer.dialog({
73
76
  - **Drawer**:长表单、字段多、需要从一侧滑出占用整面(比如设置页的「添加 / 编辑」)
74
77
  - **Dialog**:短表单、需要快速确认(比如绑定、修改密码、二次验证)
75
78
 
76
- 属性跟 `DrawerFormLayout` 完全一致,可以直接换。
79
+ 属性跟 `DrawerFormLayout` 基本一致,可以直接换。唯一区别:`DialogFormLayout` 多一个 `onCancel` 回调(Cancel 按钮和原生 X 都会触发),用于「丢弃改动」之类的确认。
77
80
 
78
81
  ### 表单字段
79
82
 
@@ -161,6 +164,7 @@ import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
161
164
  - `namespaces`:限定可选的顶层命名空间。不传就用 `flowEngine.context` 里全部已注册的
162
165
  - `extraNodes`:在命名空间过滤后追加几条静态变量(用于 `$resetLink` 这类只在当前页面有意义的局部变量)
163
166
  - `converters`:覆盖默认的 path ↔ string 转换器。`EnvVariableInput` 就是用这个钩子把输出锁定到 `$env`
167
+ - `delimiters`:变量在存储字符串里使用的开闭分隔符,默认 `['{{', '}}']`(对应 Handlebars 的 HTML 转义形式)。若字段最终以 HTML 渲染、转义会破坏变量内容(如站内信正文),传 `['{{{', '}}}']` 走 Handlebars 的原样输出形式
164
168
  - `value` / `onChange` / `placeholder` / `disabled`:标准受控字段属性
165
169
 
166
170
  底层共用 `VariableHybridInput`(`VariableInput`)和 `TextAreaWithContextSelector`(`VariableTextArea`),用同一套 MetaTree 数据。
@@ -270,6 +274,48 @@ import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';
270
274
  - `PAGE_SIZE_OPTIONS`:建议的分页选项 `[5, 10, 20, 50, 100, 200]`
271
275
  - `SortHandle`:从 `@nocobase/client-v2` 导出的独立手柄组件,可以嵌进自定义列
272
276
 
277
+ ### 筛选
278
+
279
+ #### CollectionFilter
280
+
281
+ 绑定 Collection 的筛选按钮。点击展开 Popover,里面是多条件筛选表单(字段选择器 + 操作符 + 取值控件)。Submit 收起 Popover 并通过 `onChange` 发出 NocoBase filter 参数;Reset 保持 Popover 打开并发出 `undefined`。
282
+
283
+ ```tsx
284
+ import { CollectionFilter, ExtendCollectionsProvider } from '@nocobase/client-v2';
285
+ import lockedUsersCollection from '../../collections/locked-users';
286
+
287
+ function Page() {
288
+ const main = engine.context.dataSourceManager?.getDataSource?.('main');
289
+ const collection = main?.getCollection?.(lockedUsersCollection.name);
290
+
291
+ const listRequest = useRequest(
292
+ async (filter) => api.resource('lockedUsers').list({ ...(filter ? { filter } : {}) }),
293
+ { defaultParams: [undefined] },
294
+ );
295
+
296
+ return (
297
+ <ExtendCollectionsProvider collections={[lockedUsersCollection]}>
298
+ <CollectionFilter collection={collection} onChange={listRequest.run} t={t} />
299
+ {/* table … */}
300
+ </ExtendCollectionsProvider>
301
+ );
302
+ }
303
+ ```
304
+
305
+ 主要属性:
306
+
307
+ - `collection`:作为字段来源的 Collection。`undefined` 时按钮 disabled
308
+ - `onChange: (filter) => void`:Submit 或 Reset 时触发,参数是编译好的 NocoBase filter(Reset 时为 `undefined`)。常见做法是直接转给 `listRequest.run`
309
+ - `t`:翻译函数。建议传 `useT()`(来自插件 `locale.ts`),它会自动展开服务端返回的 `{{t("…")}}` 模板,否则字段标签、操作符标签可能显示成字面模板
310
+ - `filterableFieldNames`:白名单,限制顶层可选字段
311
+ - `noIgnore`:忽略白名单
312
+ - `buttonText`:覆盖按钮文字,默认 `t('Filter')`
313
+ - `showCount`:是否在按钮上显示当前条件数 `(N)`,默认 `true`
314
+ - `popoverProps` / `buttonProps`:透传给 antd `Popover` / `Button`
315
+ - `popoverMinWidth`:Popover 内容最小宽度,默认 `520`
316
+
317
+ 要筛选的 Collection 如果是 `schema-only`(服务端没自动发布到客户端 data source),用 `<ExtendCollectionsProvider>` 包一下当前页面,让 `CollectionFilter` 能解析到。
318
+
273
319
  ### 工具
274
320
 
275
321
  #### createFormRegistry
@@ -300,6 +346,43 @@ storageTypes.unregister('local');
300
346
 
301
347
  `name` 重复注册会用新条目覆盖旧的,同时打 `console.warn`——HMR 时不抛错,开发期能看到意外的重复。
302
348
 
349
+ ## data-source/
350
+
351
+ 跟数据源 / Collection 注册相关的组件。从 `@nocobase/client-v2` 顶层 export。
352
+
353
+ ### ExtendCollectionsProvider
354
+
355
+ 挂载期 Collection 注入器。在组件挂载时把传入的 collection 注册到目标 data source,卸载时移除;会监听 `dataSource:loaded` 自动重新注入,确保数据源 reload 时不会被清掉。
356
+
357
+ ```tsx
358
+ import { ExtendCollectionsProvider } from '@nocobase/client-v2';
359
+ import lockedUsersCollection from '../../collections/locked-users';
360
+
361
+ // 模块级常量——保证引用稳定,避免 provider 每次父级重渲染都重跑 effect
362
+ const collections = [lockedUsersCollection];
363
+
364
+ export function LockedUsersPage() {
365
+ return (
366
+ <ExtendCollectionsProvider collections={collections}>
367
+ <LockedUsersPageInner />
368
+ </ExtendCollectionsProvider>
369
+ );
370
+ }
371
+ ```
372
+
373
+ 主要属性:
374
+
375
+ - `collections: CollectionOptions[]`:本次要注入的 Collection。Provider 只会注册当时不存在的那些,卸载时也只移除自己注册过的
376
+ - `dataSource`:目标 data source key,默认 `'main'`
377
+ - `children`:被注入 Collection 覆盖的子树
378
+
379
+ 什么时候用:
380
+
381
+ - 服务端 collection 是 `schema-only`,不会自动发布到客户端 data source(比如 `lockedUsers`)
382
+ - 需要一个纯客户端的 collection 镜像,只对当前页面有效,不污染全局
383
+
384
+ 常见搭配:跟 `<CollectionFilter>` 一起用——前者把 collection 挂上,后者读取并渲染筛选表单。
385
+
303
386
  ## 怎么决定加不加新组件
304
387
 
305
388
  - 出现两个及以上插件需要同一形态的字段或容器——抽到这里
@@ -7,11 +7,32 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { observable } from '@formily/reactive';
11
- import { useEffect, useMemo } from 'react';
12
- import { useLocation, useMatch, useMatches, useParams } from 'react-router-dom';
10
+ import { useEffect } from 'react';
11
+ import { useLocation, useMatches, useParams } from 'react-router-dom';
13
12
  import { Application } from '../Application';
14
13
 
14
+ type LayoutMatchLike = {
15
+ id: string;
16
+ pathname: string;
17
+ };
18
+
19
+ type LayoutDefinitionLike = {
20
+ routeName: string;
21
+ };
22
+
23
+ export function findDeepestLayoutMatch(layouts: LayoutDefinitionLike[] = [], matches: LayoutMatchLike[] = []) {
24
+ const layoutRouteNames = new Set(layouts.map((layout) => layout.routeName));
25
+
26
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
27
+ const match = matches[index];
28
+ if (layoutRouteNames.has(match.id)) {
29
+ return match;
30
+ }
31
+ }
32
+
33
+ return null;
34
+ }
35
+
15
36
  export function useRouterSync(app: Application) {
16
37
  const params = useParams();
17
38
  const location = useLocation();
@@ -20,13 +41,16 @@ export function useRouterSync(app: Application) {
20
41
  useEffect(() => {
21
42
  const last = matches[matches.length - 1];
22
43
  if (!last) return;
44
+ const layoutMatch = findDeepestLayoutMatch(app.layoutManager?.listLayouts?.(), matches);
23
45
  engine.context['_observableCache']['route'] = {
24
46
  name: last.id,
25
47
  pathname: last.pathname,
26
48
  path: last.handle?.['path'] || null,
27
49
  params,
50
+ layoutRouteName: layoutMatch?.id,
51
+ layoutBasePathname: layoutMatch?.pathname,
28
52
  };
29
- }, [engine.context, params, matches]);
53
+ }, [app, engine.context, params, matches]);
30
54
  useEffect(() => {
31
55
  engine.context['_observableCache']['location'] = location;
32
56
  }, [engine.context, location]);
@@ -0,0 +1,63 @@
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, screen, waitFor } from '@testing-library/react';
11
+ import React, { useEffect } from 'react';
12
+ import { createMemoryRouter, Outlet, RouterProvider, useParams } from 'react-router-dom';
13
+ import { describe, expect, it } from 'vitest';
14
+ import { KeepAlive } from '../KeepAlive';
15
+
16
+ describe('KeepAlive', () => {
17
+ it('keeps inactive outlet pages mounted while switching route params', async () => {
18
+ const events: string[] = [];
19
+
20
+ const Page = () => {
21
+ const { name } = useParams();
22
+ useEffect(() => {
23
+ events.push(`mount:${name}`);
24
+ return () => {
25
+ events.push(`unmount:${name}`);
26
+ };
27
+ }, [name]);
28
+
29
+ return <div>page {name}</div>;
30
+ };
31
+
32
+ const Layout = () => {
33
+ const { name } = useParams();
34
+ return <KeepAlive uid={name || ''}>{() => <Outlet />}</KeepAlive>;
35
+ };
36
+
37
+ const router = createMemoryRouter(
38
+ [
39
+ {
40
+ path: '/:name',
41
+ element: <Layout />,
42
+ children: [{ index: true, element: <Page /> }],
43
+ },
44
+ ],
45
+ {
46
+ initialEntries: ['/page-a'],
47
+ },
48
+ );
49
+
50
+ render(<RouterProvider router={router} />);
51
+
52
+ expect(await screen.findByText('page page-a')).toBeInTheDocument();
53
+
54
+ await router.navigate('/page-b');
55
+
56
+ expect(await screen.findByText('page page-b')).toBeInTheDocument();
57
+ expect(screen.getByText('page page-a')).toBeInTheDocument();
58
+
59
+ await waitFor(() => {
60
+ expect(events).toEqual(['mount:page-a', 'mount:page-b']);
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,27 @@
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 { describe, expect, it } from 'vitest';
11
+ import { findDeepestLayoutMatch } from '../RouterBridge';
12
+
13
+ describe('RouterBridge', () => {
14
+ it('uses the deepest matched layout route as layout base pathname', () => {
15
+ const match = findDeepestLayoutMatch(
16
+ [{ routeName: 'admin' }, { routeName: 'admin.settings.publicForms' }],
17
+ [
18
+ { id: 'admin', pathname: '/admin' },
19
+ { id: 'admin.settings', pathname: '/admin/settings' },
20
+ { id: 'admin.settings.publicForms', pathname: '/admin/settings/public-forms' },
21
+ { id: 'admin.settings.publicForms.page.view', pathname: '/admin/settings/public-forms/form-1/view/popup' },
22
+ ],
23
+ );
24
+
25
+ expect(match?.pathname).toBe('/admin/settings/public-forms');
26
+ });
27
+ });
@@ -16,17 +16,9 @@ export interface DialogFormLayoutProps {
16
16
  title: React.ReactNode;
17
17
  /** Form body — typically a `<Form>` wrapping `<Form.Item>` fields. */
18
18
  children: React.ReactNode;
19
- /**
20
- * Called before the dialog is closed by the Cancel button or the
21
- * top-right close (X) icon. Use for "discard changes" confirmations.
22
- */
19
+ /** Called before the dialog is closed by the Cancel button or the top-right close (X) icon. Use for "discard changes" confirmations. */
23
20
  onCancel?: () => void | Promise<void>;
24
- /**
25
- * Called when the Submit button is clicked. Caller owns validation
26
- * + the actual API call; the dialog is closed automatically when
27
- * `onSubmit` resolves. Throw from `onSubmit` to keep the dialog open
28
- * (e.g. on a validation error).
29
- */
21
+ /** Called when the Submit button is clicked. Caller owns validation + the actual API call; the dialog is closed automatically when `onSubmit` resolves. Throw from `onSubmit` to keep the dialog open (e.g. on a validation error). */
30
22
  onSubmit?: () => void | Promise<void>;
31
23
  /** Drives the Submit button's loading state. */
32
24
  submitting?: boolean;
@@ -34,30 +26,14 @@ export interface DialogFormLayoutProps {
34
26
  submitText?: React.ReactNode;
35
27
  /** Override the Cancel button label. Defaults to "Cancel". */
36
28
  cancelText?: React.ReactNode;
37
- /**
38
- * Full override of the footer content. When provided, the default
39
- * Cancel + Submit buttons are replaced. Useful for forms that need
40
- * extra actions (e.g. Preview, Save draft).
41
- */
29
+ /** Full override of the footer content. When provided, the default Cancel + Submit buttons are replaced. Useful for forms that need extra actions (e.g. Preview, Save draft). */
42
30
  footer?: React.ReactNode;
43
31
  }
44
32
 
45
33
  /**
46
- * Standard layout for dialog-hosted forms — the dialog counterpart of
47
- * `DrawerFormLayout`. Title sits left-aligned in the dialog's native
48
- * header (no inline close icon — the dialog provides its own X in the
49
- * top-right when opened with `viewer.dialog({ closable: true, ... })`),
50
- * the form body fills the middle, and a Cancel + Submit footer sits
51
- * at the bottom.
34
+ * Standard layout for dialog-hosted forms — the dialog counterpart of `DrawerFormLayout`. Title sits left-aligned in the dialog's native header, the form body fills the middle, and a Cancel + Submit footer sits at the bottom. Neither this layout nor `DrawerFormLayout` renders a close icon — both rely on the caller passing `closable: true` at the `viewer.dialog` / `viewer.drawer` call site to surface antd Modal's native top-right X (Dialog) or antd Drawer's native left-side X (Drawer).
52
35
  *
53
- * Why not just reuse `DrawerFormLayout`? `DrawerFormLayout` injects a
54
- * `<CloseOutlined>` button next to the title — that's the drawer
55
- * visual contract (close lives near the title in a side panel). In a
56
- * centered dialog the native top-right close button is the expected
57
- * affordance, so a separate layout keeps the visual contract clean.
58
- *
59
- * Callers own the `<Form>` instance, validation, and the actual API
60
- * call. This component only handles the chrome and close behaviour.
36
+ * Callers own the `<Form>` instance, validation, and the actual API call. This component only handles the chrome and close behaviour.
61
37
  *
62
38
  * Example:
63
39
  *