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

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 (338) 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/authRedirect.d.ts +12 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +51 -15
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +82 -3
  7. package/es/collection-manager/field-configure.d.ts +80 -0
  8. package/es/collection-manager/field-validation.d.ts +43 -0
  9. package/es/collection-manager/filter-operators/index.d.ts +46 -0
  10. package/es/collection-manager/filter-operators/operators.d.ts +30 -0
  11. package/es/collection-manager/interfaces/checkbox.d.ts +1 -41
  12. package/es/collection-manager/interfaces/checkboxGroup.d.ts +12 -44
  13. package/es/collection-manager/interfaces/collection.d.ts +12 -51
  14. package/es/collection-manager/interfaces/color.d.ts +1 -16
  15. package/es/collection-manager/interfaces/createdAt.d.ts +1 -44
  16. package/es/collection-manager/interfaces/createdBy.d.ts +0 -4
  17. package/es/collection-manager/interfaces/dateOnly.d.ts +7 -44
  18. package/es/collection-manager/interfaces/datetime.d.ts +1 -44
  19. package/es/collection-manager/interfaces/datetimeNoTz.d.ts +1 -44
  20. package/es/collection-manager/interfaces/email.d.ts +1 -29
  21. package/es/collection-manager/interfaces/id.d.ts +1 -16
  22. package/es/collection-manager/interfaces/index.d.ts +2 -3
  23. package/es/collection-manager/interfaces/input.d.ts +1 -102
  24. package/es/collection-manager/interfaces/integer.d.ts +1 -95
  25. package/es/collection-manager/interfaces/json.d.ts +16 -7
  26. package/es/collection-manager/interfaces/m2m.d.ts +11 -19
  27. package/es/collection-manager/interfaces/m2o.d.ts +11 -19
  28. package/es/collection-manager/interfaces/markdown.d.ts +1 -63
  29. package/es/collection-manager/interfaces/multipleSelect.d.ts +12 -44
  30. package/es/collection-manager/interfaces/nanoid.d.ts +1 -34
  31. package/es/collection-manager/interfaces/number.d.ts +1 -87
  32. package/es/collection-manager/interfaces/o2m.d.ts +12 -24
  33. package/es/collection-manager/interfaces/obo.d.ts +207 -0
  34. package/es/collection-manager/interfaces/oho.d.ts +207 -0
  35. package/es/collection-manager/interfaces/password.d.ts +1 -56
  36. package/es/collection-manager/interfaces/percent.d.ts +1 -84
  37. package/es/collection-manager/interfaces/phone.d.ts +1 -25
  38. package/es/collection-manager/interfaces/properties/index.d.ts +0 -28
  39. package/es/collection-manager/interfaces/radioGroup.d.ts +1 -29
  40. package/es/collection-manager/interfaces/richText.d.ts +1 -63
  41. package/es/collection-manager/interfaces/select.d.ts +12 -44
  42. package/es/collection-manager/interfaces/snowflake-id.d.ts +1 -34
  43. package/es/collection-manager/interfaces/tableoid.d.ts +1 -10
  44. package/es/collection-manager/interfaces/textarea.d.ts +1 -51
  45. package/es/collection-manager/interfaces/time.d.ts +1 -16
  46. package/es/collection-manager/interfaces/types.d.ts +3 -12
  47. package/es/collection-manager/interfaces/unixTimestamp.d.ts +1 -44
  48. package/es/collection-manager/interfaces/updatedAt.d.ts +1 -44
  49. package/es/collection-manager/interfaces/updatedBy.d.ts +0 -4
  50. package/es/collection-manager/interfaces/url.d.ts +1 -20
  51. package/es/collection-manager/interfaces/uuid.d.ts +1 -34
  52. package/es/collection-manager/template-fields.d.ts +53 -0
  53. package/es/components/KeepAlive.d.ts +22 -0
  54. package/es/components/RouterBridge.d.ts +9 -0
  55. package/es/components/form/DialogFormLayout.d.ts +5 -29
  56. package/es/components/form/TypedVariableInput.d.ts +75 -0
  57. package/es/components/form/VariableInput.d.ts +53 -2
  58. package/es/components/form/filter/CollectionFilter.d.ts +49 -0
  59. package/es/components/form/filter/CollectionFilterItem.d.ts +49 -0
  60. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  61. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  62. package/es/components/form/filter/index.d.ts +11 -0
  63. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  64. package/es/components/form/index.d.ts +2 -0
  65. package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
  66. package/es/data-source/index.d.ts +9 -0
  67. package/es/flow/FlowPage.d.ts +2 -1
  68. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  69. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  70. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  71. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  72. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  73. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  74. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
  75. package/es/flow/admin-shell/admin-layout/index.d.ts +3 -0
  76. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
  77. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  78. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  79. package/es/flow/components/ConditionBuilder.d.ts +1 -0
  80. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  81. package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
  82. package/es/flow/components/FlowRoute.d.ts +10 -1
  83. package/es/flow/components/fieldAssignOptions.d.ts +10 -0
  84. package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
  85. package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
  86. package/es/flow/components/filter/index.d.ts +2 -0
  87. package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
  88. package/es/flow/components/filter/useFilterOptions.d.ts +66 -0
  89. package/es/flow/index.d.ts +4 -0
  90. package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
  91. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  92. package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +92 -0
  93. package/es/flow/models/blocks/assign-form/index.d.ts +1 -0
  94. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  95. package/es/flow/models/blocks/form/FormActionModel.d.ts +9 -2
  96. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  97. package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
  98. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  99. package/es/flow-compat/passwordUtils.d.ts +1 -1
  100. package/es/index.d.ts +6 -0
  101. package/es/index.mjs +552 -459
  102. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  103. package/es/layout-manager/LayoutManager.d.ts +22 -0
  104. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  105. package/es/layout-manager/index.d.ts +13 -0
  106. package/es/layout-manager/types.d.ts +20 -0
  107. package/es/layout-manager/utils.d.ts +14 -0
  108. package/es/nocobase-buildin-plugin/index.d.ts +3 -10
  109. package/es/settings-center/index.d.ts +1 -1
  110. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  111. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  112. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  113. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  114. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  115. package/lib/index.js +552 -459
  116. package/package.json +8 -7
  117. package/src/Application.tsx +51 -12
  118. package/src/BaseApplication.tsx +32 -0
  119. package/src/PluginSettingsManager.ts +1 -1
  120. package/src/RouterManager.tsx +17 -1
  121. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  122. package/src/__tests__/app.test.tsx +17 -1
  123. package/src/__tests__/globalDeps.test.ts +1 -0
  124. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +50 -2
  125. package/src/__tests__/plugin-manager.test.tsx +177 -0
  126. package/src/__tests__/settings-center.test.tsx +24 -2
  127. package/src/authRedirect.ts +43 -0
  128. package/src/collection-field-interface/CollectionFieldInterface.ts +71 -77
  129. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +201 -4
  130. package/src/collection-manager/field-configure.ts +548 -0
  131. package/src/collection-manager/field-validation.ts +195 -0
  132. package/src/collection-manager/filter-operators/index.ts +176 -0
  133. package/src/collection-manager/{interfaces/properties → filter-operators}/operators.ts +24 -13
  134. package/src/collection-manager/interfaces/checkbox.ts +2 -9
  135. package/src/collection-manager/interfaces/checkboxGroup.ts +2 -10
  136. package/src/collection-manager/interfaces/collection.ts +2 -15
  137. package/src/collection-manager/interfaces/color.ts +2 -2
  138. package/src/collection-manager/interfaces/createdAt.ts +2 -2
  139. package/src/collection-manager/interfaces/createdBy.ts +1 -12
  140. package/src/collection-manager/interfaces/dateOnly.ts +8 -2
  141. package/src/collection-manager/interfaces/datetime.ts +2 -2
  142. package/src/collection-manager/interfaces/datetimeNoTz.ts +2 -2
  143. package/src/collection-manager/interfaces/email.ts +2 -9
  144. package/src/collection-manager/interfaces/id.ts +1 -2
  145. package/src/collection-manager/interfaces/index.ts +2 -3
  146. package/src/collection-manager/interfaces/input.ts +2 -133
  147. package/src/collection-manager/interfaces/integer.ts +2 -71
  148. package/src/collection-manager/interfaces/json.tsx +17 -11
  149. package/src/collection-manager/interfaces/m2m.tsx +0 -21
  150. package/src/collection-manager/interfaces/m2o.tsx +0 -22
  151. package/src/collection-manager/interfaces/markdown.ts +2 -51
  152. package/src/collection-manager/interfaces/multipleSelect.ts +2 -14
  153. package/src/collection-manager/interfaces/nanoid.ts +2 -2
  154. package/src/collection-manager/interfaces/number.ts +2 -85
  155. package/src/collection-manager/interfaces/o2m.tsx +1 -22
  156. package/src/collection-manager/interfaces/obo.tsx +145 -0
  157. package/src/collection-manager/interfaces/oho.tsx +145 -0
  158. package/src/collection-manager/interfaces/password.ts +2 -44
  159. package/src/collection-manager/interfaces/percent.ts +2 -74
  160. package/src/collection-manager/interfaces/phone.ts +2 -2
  161. package/src/collection-manager/interfaces/properties/index.ts +0 -133
  162. package/src/collection-manager/interfaces/radioGroup.ts +2 -2
  163. package/src/collection-manager/interfaces/richText.ts +2 -51
  164. package/src/collection-manager/interfaces/select.ts +2 -14
  165. package/src/collection-manager/interfaces/snowflake-id.ts +2 -2
  166. package/src/collection-manager/interfaces/tableoid.ts +1 -2
  167. package/src/collection-manager/interfaces/textarea.ts +2 -51
  168. package/src/collection-manager/interfaces/time.ts +2 -2
  169. package/src/collection-manager/interfaces/types.ts +4 -12
  170. package/src/collection-manager/interfaces/unixTimestamp.tsx +2 -2
  171. package/src/collection-manager/interfaces/updatedAt.ts +2 -2
  172. package/src/collection-manager/interfaces/updatedBy.ts +1 -12
  173. package/src/collection-manager/interfaces/url.ts +2 -4
  174. package/src/collection-manager/interfaces/uuid.ts +2 -2
  175. package/src/collection-manager/template-fields.ts +109 -0
  176. package/src/components/KeepAlive.tsx +131 -0
  177. package/src/components/README.md +138 -6
  178. package/src/components/README.zh-CN.md +138 -7
  179. package/src/components/RouterBridge.tsx +28 -4
  180. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  181. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  182. package/src/components/form/DialogFormLayout.tsx +5 -29
  183. package/src/components/form/TypedVariableInput.tsx +441 -0
  184. package/src/components/form/VariableInput.tsx +101 -28
  185. package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
  186. package/src/components/form/__tests__/VariableInput.test.ts +85 -0
  187. package/src/components/form/filter/CollectionFilter.tsx +111 -0
  188. package/src/components/form/filter/CollectionFilterItem.tsx +184 -0
  189. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  190. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  191. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +247 -0
  192. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  193. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  194. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  195. package/src/components/form/filter/index.ts +13 -0
  196. package/src/components/form/filter/useFilterActionProps.ts +203 -0
  197. package/src/components/form/index.tsx +2 -0
  198. package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
  199. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  200. package/src/data-source/index.ts +10 -0
  201. package/src/flow/FlowPage.tsx +35 -7
  202. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  203. package/src/flow/__tests__/FlowRoute.test.tsx +537 -3
  204. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
  205. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  206. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
  207. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  208. package/src/flow/actions/aclCheck.tsx +4 -0
  209. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  210. package/src/flow/actions/dataScope.tsx +8 -1
  211. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  212. package/src/flow/actions/linkageRules.tsx +127 -2
  213. package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
  214. package/src/flow/actions/openView.tsx +28 -4
  215. package/src/flow/actions/setTargetDataScope.tsx +1 -1
  216. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  217. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  218. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  219. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  220. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +35 -7
  221. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  222. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -13
  223. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  224. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  225. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
  226. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +76 -27
  227. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  228. package/src/flow/admin-shell/admin-layout/index.ts +3 -0
  229. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
  230. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
  231. package/src/flow/admin-shell/admin-layout/useApplications.tsx +34 -1
  232. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  233. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  234. package/src/flow/common/Markdown/Edit.tsx +32 -4
  235. package/src/flow/components/AdminLayout.tsx +4 -154
  236. package/src/flow/components/ConditionBuilder.tsx +12 -2
  237. package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
  238. package/src/flow/components/FieldAssignValueInput.tsx +8 -4
  239. package/src/flow/components/FlowRoute.tsx +105 -15
  240. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
  241. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
  242. package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
  243. package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
  244. package/src/flow/components/fieldAssignOptions.ts +23 -6
  245. package/src/flow/components/filter/LinkageFilterItem.tsx +22 -9
  246. package/src/flow/components/filter/VariableFilterItem.tsx +27 -9
  247. package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
  248. package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
  249. package/src/flow/components/filter/fieldsToOptions.ts +9 -4
  250. package/src/flow/components/filter/index.ts +3 -0
  251. package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
  252. package/src/flow/components/filter/useFilterOptions.ts +102 -0
  253. package/src/flow/index.ts +4 -0
  254. package/src/flow/models/actions/FilterActionModel.tsx +15 -3
  255. package/src/flow/models/actions/UpdateRecordActionModel.tsx +14 -95
  256. package/src/flow/models/actions/UpdateRecordActionUtils.ts +4 -7
  257. package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +116 -1
  258. package/src/flow/models/base/ActionModel.tsx +8 -1
  259. package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
  260. package/src/flow/models/base/GridModel.tsx +1 -1
  261. package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
  262. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  263. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  264. package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
  265. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  266. package/src/flow/models/base/RouteModel.tsx +1 -1
  267. package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
  268. package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
  269. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +12 -3
  270. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +77 -6
  271. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +216 -0
  272. package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +308 -0
  273. package/src/flow/models/blocks/assign-form/index.ts +1 -0
  274. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +1 -1
  275. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
  276. package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
  277. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  278. package/src/flow/models/blocks/form/FormActionModel.tsx +30 -3
  279. package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
  280. package/src/flow/models/blocks/form/FormItemModel.tsx +17 -1
  281. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  282. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  283. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
  284. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +71 -0
  285. package/src/flow/models/blocks/form/submitHandler.ts +8 -1
  286. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  287. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
  288. package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
  289. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  290. package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
  291. package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
  292. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  293. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
  294. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  295. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
  296. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  297. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  298. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  299. package/src/flow/models/fields/CollectionSelectorFieldModel.tsx +8 -2
  300. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +8 -2
  301. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  302. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  303. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  304. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  305. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
  306. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  307. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
  308. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  309. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  310. package/src/index.ts +6 -0
  311. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  312. package/src/layout-manager/LayoutManager.tsx +185 -0
  313. package/src/layout-manager/LayoutRoute.tsx +138 -0
  314. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  315. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  316. package/src/layout-manager/index.ts +14 -0
  317. package/src/layout-manager/types.ts +22 -0
  318. package/src/layout-manager/utils.ts +37 -0
  319. package/src/nocobase-buildin-plugin/index.tsx +69 -67
  320. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  321. package/src/settings-center/index.ts +1 -1
  322. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  323. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  324. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  325. package/src/settings-center/plugin-manager/index.tsx +254 -0
  326. package/src/settings-center/plugin-manager/types.ts +35 -0
  327. package/src/settings-center/utils.tsx +8 -1
  328. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  329. package/src/theme/globalStyles.ts +10 -0
  330. package/src/utils/globalDeps.ts +2 -0
  331. package/es/collection-manager/interfaces/linkTo.d.ts +0 -90
  332. package/es/collection-manager/interfaces/o2o.d.ts +0 -621
  333. package/es/collection-manager/interfaces/properties/operators.d.ts +0 -294
  334. package/es/collection-manager/interfaces/subTable.d.ts +0 -172
  335. package/src/collection-manager/interfaces/linkTo.ts +0 -120
  336. package/src/collection-manager/interfaces/o2o.tsx +0 -561
  337. package/src/collection-manager/interfaces/subTable.ts +0 -218
  338. 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,10 +164,59 @@ 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.
167
171
 
172
+ #### TypedVariableInput
173
+
174
+ Typed-constant + variable hybrid input. Ported from v1 `Variable.Input`'s `useTypedConstant` pattern: an italic `x` button on the right triggers a Cascader switcher `[Null | Constant<types> | Variable<…namespaces>]`; the left side renders the matching editor (`Input` / `InputNumber` / `Select(True/False)` / `DatePicker`) or a pill carrying the variable path.
175
+
176
+ Reach for this when a field **accepts both** a typed literal **and** a variable reference. The canonical example is `plugin-notification-email`'s SMTP `port` and `secure` fields: users can type a numeric port / boolean flag, or pass `{{ $env.SMTP_PORT }}` to read from environment variables.
177
+
178
+ ```tsx
179
+ import { TypedVariableInput } from '@nocobase/client-v2';
180
+
181
+ // Port — numeric constant + $env variable
182
+ <Form.Item name={['options', 'port']} label={t('Port')} initialValue={465}>
183
+ <TypedVariableInput
184
+ types={[['number', { min: 1, max: 65535, step: 1 }]]}
185
+ namespaces={['$env']}
186
+ />
187
+ </Form.Item>
188
+
189
+ // Secure mode — boolean constant + $env variable
190
+ <Form.Item name={['options', 'secure']} label={t('Secure')} initialValue={true}>
191
+ <TypedVariableInput types={['boolean']} namespaces={['$env']} />
192
+ </Form.Item>
193
+ ```
194
+
195
+ Key props:
196
+
197
+ - `types`: allowed constant types. Shape mirrors v1 `useTypedConstant` — pass bare type names (`['number', 'boolean']`) or `[type, editorProps]` tuples (`[['number', { min, max, step }]]`) to forward props to the underlying antd editor. Defaults to `['string', 'number', 'boolean', 'date']`. **Even when only one type is allowed, the `Constant` entry still expands into a typed submenu** (Number / Boolean / Date / String) — matches v1 so users can see what type the constant is
198
+ - `namespaces`: restrict the variable picker to specific top-level namespaces (e.g. `['$env']`). Omit to expose every namespace registered on `flowEngine.context`
199
+ - `extraNodes`: static leaves appended after the namespace-filtered nodes
200
+ - `nullable`: whether to expose the `Null` switcher entry. Default `true`. Combined with `Form.Item.rules={[{ required: true }]}`, the user can explicitly clear the field but submission is still blocked by validation — mirrors v1's "Null + required" pairing
201
+ - `delimiters`: variable-token delimiters, default `['{{', '}}']` — same as `VariableInput`
202
+ - `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`: standard controlled-input props
203
+
204
+ Value shape:
205
+
206
+ - Constant: stored as the native type (`number` / `boolean` / `Date` / `string`)
207
+ - Variable: a string like `'{{ $env.SMTP_PORT }}'`
208
+ - Null: `null`
209
+
210
+ When **not** to use it:
211
+
212
+ - **Pure literal fields** (users will never pass a variable) → use the antd primitive directly (`InputNumber` / `Select` / `DatePicker` / `Input`) and skip the Cascader column overhead
213
+ - **Pure variable fields** (users will never pass a literal) → use `EnvVariableInput` (`$env`-only, with optional password masking) or `VariableInput` (general-purpose)
214
+
215
+ Capabilities skipped (present in v1, not yet ported to v2):
216
+
217
+ - `object` constant type (JSON editor) — v2 has no inline "JSON editor + Cascader switcher" yet; add when there's a concrete caller
218
+ - Async `loadChildren` cascading — most MetaTree namespaces are already eagerly resolved by `useFilteredMetaTree`, so this hasn't been needed
219
+
168
220
  #### FileSizeInput
169
221
 
170
222
  A byte-valued size input paired with a unit selector (Byte / KB / MB / GB). The persisted value is always in bytes; the displayed number is derived from the picked unit.
@@ -272,6 +324,48 @@ Companion exports:
272
324
  - `PAGE_SIZE_OPTIONS`: suggested page-size dropdown values `[5, 10, 20, 50, 100, 200]`
273
325
  - `SortHandle`: standalone handle component, exported from `@nocobase/client-v2` for embedding into custom columns
274
326
 
327
+ ### Filter
328
+
329
+ #### CollectionFilter
330
+
331
+ 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`.
332
+
333
+ ```tsx
334
+ import { CollectionFilter, ExtendCollectionsProvider } from '@nocobase/client-v2';
335
+ import lockedUsersCollection from '../../collections/locked-users';
336
+
337
+ function Page() {
338
+ const main = engine.context.dataSourceManager?.getDataSource?.('main');
339
+ const collection = main?.getCollection?.(lockedUsersCollection.name);
340
+
341
+ const listRequest = useRequest(
342
+ async (filter) => api.resource('lockedUsers').list({ ...(filter ? { filter } : {}) }),
343
+ { defaultParams: [undefined] },
344
+ );
345
+
346
+ return (
347
+ <ExtendCollectionsProvider collections={[lockedUsersCollection]}>
348
+ <CollectionFilter collection={collection} onChange={listRequest.run} t={t} />
349
+ {/* table … */}
350
+ </ExtendCollectionsProvider>
351
+ );
352
+ }
353
+ ```
354
+
355
+ Key props:
356
+
357
+ - `collection`: the Collection that drives the field picker. The button is disabled while it's `undefined`
358
+ - `onChange: (filter) => void`: fired on Submit and Reset with the compiled NocoBase filter (`undefined` on Reset). Most pages forward straight to `listRequest.run`
359
+ - `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
360
+ - `filterableFieldNames`: whitelist of root-level field names to expose
361
+ - `noIgnore`: bypass the whitelist
362
+ - `buttonText`: override the trigger label; defaults to `t('Filter')`
363
+ - `showCount`: show the `(N)` condition-count badge on the trigger; defaults to `true`
364
+ - `popoverProps` / `buttonProps`: pass-through to the antd `Popover` / `Button`
365
+ - `popoverMinWidth`: min-width of the popover body; defaults to `520`
366
+
367
+ 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.
368
+
275
369
  ### Utilities
276
370
 
277
371
  #### createFormRegistry
@@ -302,6 +396,44 @@ Use this when a plugin needs an extension point for "same name + same shape + di
302
396
 
303
397
  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
398
 
399
+ ## data-source/
400
+
401
+ Components that wire collections / data sources into the React tree. Exported from the top level of `@nocobase/client-v2`.
402
+
403
+ ### ExtendCollectionsProvider
404
+
405
+ 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.
406
+
407
+ ```tsx
408
+ import { ExtendCollectionsProvider } from '@nocobase/client-v2';
409
+ import lockedUsersCollection from '../../collections/locked-users';
410
+
411
+ // Module-level constant — keeps the reference stable so the provider's
412
+ // effect doesn't re-run on every parent re-render.
413
+ const collections = [lockedUsersCollection];
414
+
415
+ export function LockedUsersPage() {
416
+ return (
417
+ <ExtendCollectionsProvider collections={collections}>
418
+ <LockedUsersPageInner />
419
+ </ExtendCollectionsProvider>
420
+ );
421
+ }
422
+ ```
423
+
424
+ Key props:
425
+
426
+ - `collections: CollectionOptions[]`: collections to inject. The provider only adds names that aren't already present, and on unmount removes only the ones it added
427
+ - `dataSource`: target data source key; defaults to `'main'`
428
+ - `children`: subtree covered by the injection
429
+
430
+ When to use:
431
+
432
+ - The server-side collection is `schema-only` and doesn't get auto-published to the client data source (e.g. `lockedUsers`)
433
+ - You need a client-side mirror that should be visible only inside the current page, not registered globally
434
+
435
+ Typical pairing: use together with `<CollectionFilter>` — the provider makes the collection resolvable; the filter button consumes it.
436
+
305
437
  ## When to add a new component here
306
438
 
307
439
  - 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,10 +164,59 @@ 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 数据。
167
171
 
172
+ #### TypedVariableInput
173
+
174
+ 类型化常量 + 变量混合输入器。移植 v1 `Variable.Input` 的 `useTypedConstant` 形态:右侧斜体 `x` 按钮触发 Cascader 切换 `[空值 | 常量<types> | 变量和密钥<…namespaces>]`,左侧根据当前模式渲染对应编辑器(`Input` / `InputNumber` / `Select(True/False)` / `DatePicker`)或一颗带变量路径的 pill。
175
+
176
+ 用于字段**同时接受**字面量**和**变量引用的场景。最典型的就是 `plugin-notification-email` 的 SMTP `port` 和 `secure`:可以填具体数字 / 布尔值,也可以填 `{{ $env.SMTP_PORT }}` 走环境变量。
177
+
178
+ ```tsx
179
+ import { TypedVariableInput } from '@nocobase/client-v2';
180
+
181
+ // 端口:数字常量 + $env 变量
182
+ <Form.Item name={['options', 'port']} label={t('端口')} initialValue={465}>
183
+ <TypedVariableInput
184
+ types={[['number', { min: 1, max: 65535, step: 1 }]]}
185
+ namespaces={['$env']}
186
+ />
187
+ </Form.Item>
188
+
189
+ // 安全模式:布尔常量 + $env 变量
190
+ <Form.Item name={['options', 'secure']} label={t('安全模式')} initialValue={true}>
191
+ <TypedVariableInput types={['boolean']} namespaces={['$env']} />
192
+ </Form.Item>
193
+ ```
194
+
195
+ 主要属性:
196
+
197
+ - `types`:允许的常量类型。形态对齐 v1 `useTypedConstant`,可以传裸类型名 `['number', 'boolean']`,也可以传 `[type, editorProps]` 元组 `[['number', { min, max, step }]]` 把 props 透传给底层 antd 编辑器。默认 `['string', 'number', 'boolean', 'date']`。**即使只允许一种类型,「常量」入口也会展开二级菜单**(数字 / 逻辑值 / 日期 / 字符串)——跟 v1 一致,让用户能直观看到当前常量是什么类型
198
+ - `namespaces`:限定变量 picker 可选的顶层命名空间(如 `['$env']`)。不传就用 `flowEngine.context` 里所有已注册命名空间
199
+ - `extraNodes`:在命名空间过滤后追加几条静态变量节点
200
+ - `nullable`:是否暴露「空值」入口,默认 `true`。配合 `Form.Item.rules={[{ required: true }]}` 可以让用户能手动清空、但提交时会被校验拦截——跟 v1 的「空值 + required」组合一致
201
+ - `delimiters`:变量 token 开闭分隔符,默认 `['{{', '}}']`,跟 `VariableInput` 一致
202
+ - `value` / `onChange` / `placeholder` / `disabled` / `style` / `className`:标准受控字段属性
203
+
204
+ 值的形态:
205
+
206
+ - 常量:原生类型直接存(`number` / `boolean` / `Date` / `string`)
207
+ - 变量:字符串 `'{{ $env.SMTP_PORT }}'`
208
+ - 空值:`null`
209
+
210
+ 什么时候**不该用**:
211
+
212
+ - **纯字面量字段**(用户不会想填变量)→ 直接用 antd `InputNumber` / `Select` / `DatePicker` / `Input`,省掉 Cascader 那一格的视觉开销
213
+ - **纯变量字段**(用户不会想填字面量)→ 用 `EnvVariableInput`(`$env` 专用,带 password mask)或 `VariableInput`(更通用)
214
+
215
+ 跳过的能力(v1 有但 v2 还没补):
216
+
217
+ - `object` 类型(JSON 编辑器)——v2 还没对应的「内联 JSON 编辑器 + Cascader 切换」组件,等真有需求再补
218
+ - 异步 `loadChildren` 分支——大多数命名空间的 MetaTree 已经由 `useFilteredMetaTree` 提前展平,没遇到刚需
219
+
168
220
  #### FileSizeInput
169
221
 
170
222
  文件大小输入器。值统一存字节数,UI 上配一个单位选择器(Byte / KB / MB / GB)。
@@ -270,6 +322,48 @@ import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';
270
322
  - `PAGE_SIZE_OPTIONS`:建议的分页选项 `[5, 10, 20, 50, 100, 200]`
271
323
  - `SortHandle`:从 `@nocobase/client-v2` 导出的独立手柄组件,可以嵌进自定义列
272
324
 
325
+ ### 筛选
326
+
327
+ #### CollectionFilter
328
+
329
+ 绑定 Collection 的筛选按钮。点击展开 Popover,里面是多条件筛选表单(字段选择器 + 操作符 + 取值控件)。Submit 收起 Popover 并通过 `onChange` 发出 NocoBase filter 参数;Reset 保持 Popover 打开并发出 `undefined`。
330
+
331
+ ```tsx
332
+ import { CollectionFilter, ExtendCollectionsProvider } from '@nocobase/client-v2';
333
+ import lockedUsersCollection from '../../collections/locked-users';
334
+
335
+ function Page() {
336
+ const main = engine.context.dataSourceManager?.getDataSource?.('main');
337
+ const collection = main?.getCollection?.(lockedUsersCollection.name);
338
+
339
+ const listRequest = useRequest(
340
+ async (filter) => api.resource('lockedUsers').list({ ...(filter ? { filter } : {}) }),
341
+ { defaultParams: [undefined] },
342
+ );
343
+
344
+ return (
345
+ <ExtendCollectionsProvider collections={[lockedUsersCollection]}>
346
+ <CollectionFilter collection={collection} onChange={listRequest.run} t={t} />
347
+ {/* table … */}
348
+ </ExtendCollectionsProvider>
349
+ );
350
+ }
351
+ ```
352
+
353
+ 主要属性:
354
+
355
+ - `collection`:作为字段来源的 Collection。`undefined` 时按钮 disabled
356
+ - `onChange: (filter) => void`:Submit 或 Reset 时触发,参数是编译好的 NocoBase filter(Reset 时为 `undefined`)。常见做法是直接转给 `listRequest.run`
357
+ - `t`:翻译函数。建议传 `useT()`(来自插件 `locale.ts`),它会自动展开服务端返回的 `{{t("…")}}` 模板,否则字段标签、操作符标签可能显示成字面模板
358
+ - `filterableFieldNames`:白名单,限制顶层可选字段
359
+ - `noIgnore`:忽略白名单
360
+ - `buttonText`:覆盖按钮文字,默认 `t('Filter')`
361
+ - `showCount`:是否在按钮上显示当前条件数 `(N)`,默认 `true`
362
+ - `popoverProps` / `buttonProps`:透传给 antd `Popover` / `Button`
363
+ - `popoverMinWidth`:Popover 内容最小宽度,默认 `520`
364
+
365
+ 要筛选的 Collection 如果是 `schema-only`(服务端没自动发布到客户端 data source),用 `<ExtendCollectionsProvider>` 包一下当前页面,让 `CollectionFilter` 能解析到。
366
+
273
367
  ### 工具
274
368
 
275
369
  #### createFormRegistry
@@ -300,6 +394,43 @@ storageTypes.unregister('local');
300
394
 
301
395
  `name` 重复注册会用新条目覆盖旧的,同时打 `console.warn`——HMR 时不抛错,开发期能看到意外的重复。
302
396
 
397
+ ## data-source/
398
+
399
+ 跟数据源 / Collection 注册相关的组件。从 `@nocobase/client-v2` 顶层 export。
400
+
401
+ ### ExtendCollectionsProvider
402
+
403
+ 挂载期 Collection 注入器。在组件挂载时把传入的 collection 注册到目标 data source,卸载时移除;会监听 `dataSource:loaded` 自动重新注入,确保数据源 reload 时不会被清掉。
404
+
405
+ ```tsx
406
+ import { ExtendCollectionsProvider } from '@nocobase/client-v2';
407
+ import lockedUsersCollection from '../../collections/locked-users';
408
+
409
+ // 模块级常量——保证引用稳定,避免 provider 每次父级重渲染都重跑 effect
410
+ const collections = [lockedUsersCollection];
411
+
412
+ export function LockedUsersPage() {
413
+ return (
414
+ <ExtendCollectionsProvider collections={collections}>
415
+ <LockedUsersPageInner />
416
+ </ExtendCollectionsProvider>
417
+ );
418
+ }
419
+ ```
420
+
421
+ 主要属性:
422
+
423
+ - `collections: CollectionOptions[]`:本次要注入的 Collection。Provider 只会注册当时不存在的那些,卸载时也只移除自己注册过的
424
+ - `dataSource`:目标 data source key,默认 `'main'`
425
+ - `children`:被注入 Collection 覆盖的子树
426
+
427
+ 什么时候用:
428
+
429
+ - 服务端 collection 是 `schema-only`,不会自动发布到客户端 data source(比如 `lockedUsers`)
430
+ - 需要一个纯客户端的 collection 镜像,只对当前页面有效,不污染全局
431
+
432
+ 常见搭配:跟 `<CollectionFilter>` 一起用——前者把 collection 挂上,后者读取并渲染筛选表单。
433
+
303
434
  ## 怎么决定加不加新组件
304
435
 
305
436
  - 出现两个及以上插件需要同一形态的字段或容器——抽到这里
@@ -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
+ });