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

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 (334) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/PluginSettingsManager.d.ts +33 -0
  4. package/es/RouteRepository.d.ts +29 -6
  5. package/es/RouterManager.d.ts +15 -0
  6. package/es/authRedirect.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  8. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  9. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  10. package/es/components/category-tabs/index.d.ts +9 -0
  11. package/es/components/form/JsonTextArea.d.ts +2 -1
  12. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  13. package/es/components/form/TypedVariableInput.d.ts +31 -5
  14. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  15. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  16. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  17. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  18. package/es/components/form/filter/index.d.ts +2 -0
  19. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  20. package/es/components/form/index.d.ts +1 -0
  21. package/es/components/index.d.ts +2 -0
  22. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  23. package/es/entry-actions/index.d.ts +9 -0
  24. package/es/flow/actions/afterSuccess.d.ts +8 -1
  25. package/es/flow/actions/index.d.ts +2 -2
  26. package/es/flow/actions/linkageRules.d.ts +2 -0
  27. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  28. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  29. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  30. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  31. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
  32. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  33. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  34. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  35. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  36. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  37. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  38. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  39. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  40. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  41. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  42. package/es/flow/components/FlowRoute.d.ts +2 -2
  43. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  44. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  45. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  46. package/es/flow/index.d.ts +1 -0
  47. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  48. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  49. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  50. package/es/flow/models/base/GridModel.d.ts +1 -1
  51. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  52. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  53. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  54. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  55. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  56. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  57. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  58. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  59. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  60. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  61. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  62. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  63. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  64. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  65. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  66. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  67. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  68. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  69. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  70. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  71. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  72. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  73. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  74. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  75. package/es/flow/utils/formValueDeps.d.ts +32 -0
  76. package/es/flow-compat/FieldValidation.d.ts +2 -1
  77. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  78. package/es/flow-compat/routeTypes.d.ts +1 -0
  79. package/es/index.d.ts +8 -1
  80. package/es/index.mjs +367 -181
  81. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  82. package/es/utils/index.d.ts +1 -0
  83. package/es/utils/markdownSanitize.d.ts +11 -0
  84. package/lib/index.js +367 -181
  85. package/lib/locale/languageCodes.js +3 -1
  86. package/package.json +9 -7
  87. package/src/APIClient.ts +1 -10
  88. package/src/Application.tsx +4 -0
  89. package/src/BaseApplication.tsx +13 -6
  90. package/src/PluginSettingsManager.ts +53 -0
  91. package/src/RouteRepository.ts +151 -24
  92. package/src/RouterManager.tsx +142 -1
  93. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  94. package/src/__tests__/RouteRepository.test.ts +239 -0
  95. package/src/__tests__/RouterManager.test.ts +125 -0
  96. package/src/__tests__/app.test.tsx +73 -0
  97. package/src/__tests__/authRedirect.test.ts +17 -0
  98. package/src/__tests__/browserChecker.test.ts +61 -0
  99. package/src/__tests__/exports.test.ts +16 -0
  100. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  101. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +234 -2
  102. package/src/__tests__/plugin-manager.test.tsx +44 -2
  103. package/src/__tests__/settings-center.test.tsx +40 -1
  104. package/src/authRedirect.ts +38 -0
  105. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  106. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  107. package/src/collection-manager/field-configure.ts +1 -1
  108. package/src/collection-manager/field-validation.ts +1 -1
  109. package/src/collection-manager/interfaces/id.ts +1 -1
  110. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  111. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  112. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  113. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  114. package/src/collection-manager/interfaces/obo.tsx +2 -2
  115. package/src/collection-manager/interfaces/oho.tsx +2 -2
  116. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  117. package/src/collection-manager/interfaces/uuid.ts +1 -1
  118. package/src/components/AppComponents.tsx +19 -3
  119. package/src/components/README.md +7 -1
  120. package/src/components/README.zh-CN.md +6 -1
  121. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  122. package/src/components/category-tabs/index.ts +10 -0
  123. package/src/components/form/JsonTextArea.tsx +25 -11
  124. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  125. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  126. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  127. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  128. package/src/components/form/TypedVariableInput.tsx +452 -101
  129. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  130. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  131. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  132. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  133. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  134. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  135. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  136. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  137. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  138. package/src/components/form/filter/index.ts +2 -0
  139. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  140. package/src/components/form/index.tsx +1 -0
  141. package/src/components/index.ts +2 -0
  142. package/src/entry-actions/EntryActionManager.ts +76 -0
  143. package/src/entry-actions/index.ts +10 -0
  144. package/src/flow/FlowPage.tsx +38 -3
  145. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  146. package/src/flow/__tests__/FlowRoute.test.tsx +1181 -80
  147. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  148. package/src/flow/__tests__/getKey.test.ts +7 -0
  149. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  150. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +115 -0
  151. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  152. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  153. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1124 -0
  154. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  155. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  156. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  157. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  158. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  159. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  160. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  161. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  162. package/src/flow/actions/afterSuccess.tsx +142 -3
  163. package/src/flow/actions/customVariable.tsx +1 -2
  164. package/src/flow/actions/dataScopeFilter.ts +20 -1
  165. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  166. package/src/flow/actions/index.ts +3 -1
  167. package/src/flow/actions/linkageRules.tsx +209 -47
  168. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  169. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  170. package/src/flow/actions/openView.tsx +59 -5
  171. package/src/flow/actions/runjs.tsx +2 -14
  172. package/src/flow/actions/validation.tsx +62 -30
  173. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  174. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  175. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  176. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -14
  177. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  178. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  179. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  180. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  181. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  182. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  183. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  184. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  185. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  186. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  187. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
  188. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  189. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  190. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  191. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  192. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  193. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  194. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  195. package/src/flow/common/Markdown/Display.tsx +14 -3
  196. package/src/flow/common/Markdown/Edit.tsx +19 -7
  197. package/src/flow/common/Markdown/style.ts +4 -0
  198. package/src/flow/components/BlockItemCard.tsx +2 -2
  199. package/src/flow/components/DefaultValue.tsx +1 -2
  200. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  201. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  202. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  203. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  204. package/src/flow/components/FlowRoute.tsx +256 -53
  205. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  206. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  207. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  208. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  209. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  210. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  211. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  212. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  213. package/src/flow/components/fieldAssignOptions.ts +155 -27
  214. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  215. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  216. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  217. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  218. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  219. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  220. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  221. package/src/flow/index.ts +4 -1
  222. package/src/flow/internal/components/Markdown/util.ts +2 -1
  223. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  224. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  225. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  226. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  227. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  228. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  229. package/src/flow/models/base/ActionModel.tsx +21 -8
  230. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  231. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  232. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  233. package/src/flow/models/base/GridModel.tsx +38 -7
  234. package/src/flow/models/base/PageModel/PageModel.tsx +387 -50
  235. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  236. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  237. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +817 -13
  238. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  239. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  240. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  241. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  242. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  243. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  244. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  245. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  246. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  247. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +59 -6
  248. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  249. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  250. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  251. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +143 -4
  252. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  253. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  254. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  255. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  256. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  257. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  258. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  259. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  260. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  261. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  262. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  263. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  264. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  265. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  266. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  267. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  268. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  269. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  270. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +38 -14
  271. package/src/flow/models/blocks/table/TableBlockModel.tsx +42 -19
  272. package/src/flow/models/blocks/table/TableColumnModel.tsx +72 -6
  273. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  274. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  275. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  276. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  277. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  278. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +243 -1
  279. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  280. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  281. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  282. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  283. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +31 -7
  284. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  285. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  286. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  287. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  288. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  289. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  290. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  291. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  292. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  293. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  294. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  295. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  296. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  297. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  298. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  299. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  300. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  301. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  302. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  303. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  304. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  305. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  306. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  307. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  308. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  309. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  310. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  311. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  312. package/src/flow/routeTransientInputArgs.ts +27 -0
  313. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  314. package/src/flow/utils/dataScopeFormValueClear.ts +219 -81
  315. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  316. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  317. package/src/flow/utils/formValueDeps.ts +170 -0
  318. package/src/flow-compat/FieldValidation.tsx +122 -60
  319. package/src/flow-compat/Popover.tsx +43 -4
  320. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  321. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  322. package/src/flow-compat/routeTypes.ts +1 -0
  323. package/src/index.ts +14 -1
  324. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  325. package/src/locale/languageCodes.ts +3 -1
  326. package/src/nocobase-buildin-plugin/index.tsx +63 -15
  327. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  328. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  329. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  330. package/src/settings-center/plugin-manager/index.tsx +3 -0
  331. package/src/settings-center/utils.tsx +0 -6
  332. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  333. package/src/utils/index.tsx +1 -0
  334. package/src/utils/markdownSanitize.ts +88 -0
@@ -12,6 +12,10 @@ import { act, render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
13
  import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
14
14
 
15
+ const TestingAppSpin = () => <div data-testid="app-spin">app spin</div>;
16
+
17
+ const TestingAppError = ({ error }: { error: Error }) => <div role="alert">{error.message}</div>;
18
+
15
19
  class SkippedPublicRoutePlugin extends Plugin {
16
20
  async load() {
17
21
  this.router.add('public', {
@@ -22,10 +26,36 @@ class SkippedPublicRoutePlugin extends Plugin {
22
26
  }
23
27
  }
24
28
 
29
+ class DummySigninRoutePlugin extends Plugin {
30
+ async load() {
31
+ this.router.add('signin-test', {
32
+ path: '/signin',
33
+ skipAuthCheck: true,
34
+ Component: () => <div>signin page</div>,
35
+ });
36
+ }
37
+ }
38
+
39
+ class AuthBootstrapRoutePlugin extends Plugin {
40
+ async load() {
41
+ this.router.add('secure', {
42
+ path: '/secure',
43
+ authCheck: true,
44
+ Component: () => <div>secure page</div>,
45
+ });
46
+ this.router.add('guest', {
47
+ path: '/guest',
48
+ authCheck: false,
49
+ Component: () => <div>guest page</div>,
50
+ });
51
+ }
52
+ }
53
+
25
54
  describe('nocobase buildin plugin auth redirect', () => {
26
55
  const originalLocation = globalThis.window.location;
27
56
 
28
57
  beforeEach(() => {
58
+ globalThis.window.localStorage.clear();
29
59
  // These fixtures mount the modern client under the `v2` segment; tell the
30
60
  // runtime-prefix helper so v2-runtime detection matches (server injects it
31
61
  // in production).
@@ -46,6 +76,7 @@ describe('nocobase buildin plugin auth redirect', () => {
46
76
  });
47
77
 
48
78
  afterEach(() => {
79
+ globalThis.window.localStorage.clear();
49
80
  delete (globalThis.window as any).__nocobase_modern_client_prefix__;
50
81
  Object.defineProperty(globalThis.window, 'location', {
51
82
  configurable: true,
@@ -87,6 +118,7 @@ describe('nocobase buildin plugin auth redirect', () => {
87
118
  plugins: [NocoBaseBuildInPlugin as any],
88
119
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
89
120
  });
121
+ app.apiClient.auth.setToken('test-token');
90
122
  app.apiMock.onGet('app:getLang').reply(200, {
91
123
  data: { lang: 'en-US', resources: { client: {} }, cron: {} },
92
124
  });
@@ -101,6 +133,7 @@ describe('nocobase buildin plugin auth redirect', () => {
101
133
  await new Promise((resolve) => setTimeout(resolve, 50));
102
134
  expect(app.router.router.state.location.pathname).toBe('/v2/admin');
103
135
  expect(app.router.router.state.location.search).toBe('');
136
+ expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(0);
104
137
  });
105
138
 
106
139
  it('should redirect unauthenticated v2 root access to v2 signin via <Navigate />', async () => {
@@ -155,6 +188,200 @@ describe('nocobase buildin plugin auth redirect', () => {
155
188
  });
156
189
  });
157
190
 
191
+ it('should defer data source bootstrap until auth-required route is authenticated after signin redirect', async () => {
192
+ const app = createMockClient({
193
+ publicPath: '/v2/',
194
+ plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, AuthBootstrapRoutePlugin as any],
195
+ components: { AppSpin: TestingAppSpin },
196
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
197
+ });
198
+ app.apiMock.onGet('app:getLang').reply(200, {
199
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
200
+ });
201
+ const events: string[] = [];
202
+ app.apiMock.onGet('/auth:check').replyOnce(() => {
203
+ events.push('auth:first');
204
+ return [200, { data: {} }];
205
+ });
206
+ app.apiMock.onGet('/auth:check').reply(() => {
207
+ events.push('auth:second');
208
+ return [200, { data: { id: 1 } }];
209
+ });
210
+
211
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockImplementation(() => {
212
+ if (!app.apiClient.auth.token) {
213
+ const pending = new Promise<void>(() => undefined);
214
+ app.dataSourceManager.loadingPromise = pending;
215
+ return pending;
216
+ }
217
+ events.push('bootstrap');
218
+ expect(events).toContain('auth:second');
219
+ return Promise.resolve();
220
+ });
221
+
222
+ const Root = app.getRootComponent();
223
+ render(<Root />);
224
+
225
+ await waitFor(() => {
226
+ expect(app.router.router.state.location.pathname).toBe('/v2/signin');
227
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fsecure');
228
+ });
229
+ expect(await screen.findByText('signin page')).toBeInTheDocument();
230
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
231
+ expect(ensureLoaded).not.toHaveBeenCalled();
232
+ expect(app.dataSourceManager.loadingPromise).toBeNull();
233
+ expect(events).toEqual(['auth:first']);
234
+
235
+ act(() => {
236
+ app.apiClient.auth.setToken('test-token');
237
+ });
238
+
239
+ await act(async () => {
240
+ await app.router.router.navigate('/secure');
241
+ });
242
+
243
+ expect(await screen.findByText('secure page')).toBeInTheDocument();
244
+ await waitFor(() => {
245
+ expect(document.querySelector('.ant-spin-spinning')).not.toBeInTheDocument();
246
+ });
247
+ expect(ensureLoaded).toHaveBeenCalledTimes(1);
248
+ });
249
+
250
+ it('should redirect on auth-required /auth:check 401 without bootstrapping data sources', async () => {
251
+ const app = createMockClient({
252
+ publicPath: '/v2/',
253
+ plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, AuthBootstrapRoutePlugin as any],
254
+ components: { AppSpin: TestingAppSpin },
255
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
256
+ });
257
+ app.apiMock.onGet('app:getLang').reply(200, {
258
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
259
+ });
260
+ app.apiMock.onGet('/auth:check').reply(401, { errors: [{ code: 'EMPTY_TOKEN' }] });
261
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockImplementation(() => {
262
+ const pending = new Promise<void>(() => undefined);
263
+ app.dataSourceManager.loadingPromise = pending;
264
+ return pending;
265
+ });
266
+
267
+ const Root = app.getRootComponent();
268
+ render(<Root />);
269
+
270
+ await waitFor(() => {
271
+ expect(app.router.router.state.location.pathname).toBe('/v2/signin');
272
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fsecure');
273
+ });
274
+ expect(await screen.findByText('signin page')).toBeInTheDocument();
275
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
276
+ expect(ensureLoaded).not.toHaveBeenCalled();
277
+ expect(app.dataSourceManager.loadingPromise).toBeNull();
278
+ });
279
+
280
+ it('should surface non-auth /auth:check errors instead of leaving auth-required route spinning', async () => {
281
+ const app = createMockClient({
282
+ publicPath: '/v2/',
283
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
284
+ components: { AppSpin: TestingAppSpin, AppError: TestingAppError },
285
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
286
+ });
287
+ app.apiMock.onGet('app:getLang').reply(200, {
288
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
289
+ });
290
+ app.apiMock.onGet('/auth:check').reply(500, { errors: [{ message: 'auth check failed' }] });
291
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
292
+
293
+ const Root = app.getRootComponent();
294
+ render(<Root />);
295
+
296
+ expect(await screen.findByRole('alert')).toHaveTextContent('Request failed with status code 500');
297
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
298
+ expect(ensureLoaded).not.toHaveBeenCalled();
299
+ });
300
+
301
+ it.each(['/v2/signin', '/v2/public'])('should not bootstrap data sources on skipped auth route: %s', async (path) => {
302
+ const app = createMockClient({
303
+ publicPath: '/v2/',
304
+ plugins: [NocoBaseBuildInPlugin as any, DummySigninRoutePlugin as any, SkippedPublicRoutePlugin as any],
305
+ components: { AppSpin: TestingAppSpin },
306
+ router: { type: 'memory', initialEntries: [path] },
307
+ });
308
+ app.apiMock.onGet('app:getLang').reply(200, {
309
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
310
+ });
311
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
312
+
313
+ const Root = app.getRootComponent();
314
+ render(<Root />);
315
+
316
+ expect(await screen.findByText(path === '/v2/signin' ? 'signin page' : 'public page')).toBeInTheDocument();
317
+ expect(screen.queryByTestId('app-spin')).not.toBeInTheDocument();
318
+ expect(ensureLoaded).not.toHaveBeenCalled();
319
+ expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check')).toHaveLength(0);
320
+ });
321
+
322
+ it('should bootstrap data sources for authenticated auth-required route access', async () => {
323
+ const app = createMockClient({
324
+ publicPath: '/v2/',
325
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
326
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
327
+ });
328
+ app.apiClient.auth.setToken('test-token');
329
+ app.apiMock.onGet('app:getLang').reply(200, {
330
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
331
+ });
332
+ app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
333
+ app.apiMock.onPost('auth:syncCookies').reply(200, { data: { synced: true } });
334
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
335
+
336
+ const Root = app.getRootComponent();
337
+ render(<Root />);
338
+
339
+ expect(await screen.findByText('secure page')).toBeInTheDocument();
340
+ expect(ensureLoaded).toHaveBeenCalledTimes(1);
341
+ expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(1);
342
+ });
343
+
344
+ it('should keep authenticated v2 startup working when cookie bootstrap fails', async () => {
345
+ const app = createMockClient({
346
+ publicPath: '/v2/',
347
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
348
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
349
+ });
350
+ app.apiClient.auth.setToken('test-token');
351
+ app.apiMock.onGet('app:getLang').reply(200, {
352
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
353
+ });
354
+ app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
355
+ app.apiMock.onPost('auth:syncCookies').reply(500, { errors: [{ message: 'cookie sync failed' }] });
356
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
357
+
358
+ const Root = app.getRootComponent();
359
+ render(<Root />);
360
+
361
+ expect(await screen.findByText('secure page')).toBeInTheDocument();
362
+ expect(ensureLoaded).toHaveBeenCalledTimes(1);
363
+ expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(1);
364
+ });
365
+
366
+ it('should not auth-check or bootstrap authCheck false routes', async () => {
367
+ const app = createMockClient({
368
+ publicPath: '/v2/',
369
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
370
+ router: { type: 'memory', initialEntries: ['/v2/guest'] },
371
+ });
372
+ app.apiMock.onGet('app:getLang').reply(200, {
373
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
374
+ });
375
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
376
+
377
+ const Root = app.getRootComponent();
378
+ render(<Root />);
379
+
380
+ expect(await screen.findByText('guest page')).toBeInTheDocument();
381
+ expect(ensureLoaded).not.toHaveBeenCalled();
382
+ expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check')).toHaveLength(0);
383
+ });
384
+
158
385
  it('should render v2 admin root without redirecting away', async () => {
159
386
  const app = createMockClient({
160
387
  publicPath: '/v2/',
@@ -188,7 +415,7 @@ describe('nocobase buildin plugin auth redirect', () => {
188
415
  });
189
416
 
190
417
  it.each(['/v2/admin/legacy-page/tab/tab-1', '/v2/admin/legacy-page/view/detail'])(
191
- 'should show 404 for authenticated direct v1-style v2 page access: %s',
418
+ 'should explain authenticated direct v1-style v2 page access: %s',
192
419
  async (pathname) => {
193
420
  const app = createMockClient({
194
421
  publicPath: '/v2/',
@@ -214,7 +441,12 @@ describe('nocobase buildin plugin auth redirect', () => {
214
441
  const Root = app.getRootComponent();
215
442
  render(<Root />);
216
443
 
217
- expect(await screen.findByText('404')).toBeInTheDocument();
444
+ expect(await screen.findByText('This page is not supported in the /v2/ branch')).toBeInTheDocument();
445
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
446
+ expect(screen.getByRole('link', { name: 'Open from the original entry' })).toHaveAttribute(
447
+ 'href',
448
+ pathname.replace(/^\/v2/, ''),
449
+ );
218
450
  expect(app.router.router.state.location.pathname).toBe(pathname);
219
451
  },
220
452
  );
@@ -18,6 +18,19 @@ class TestAclPlugin extends Plugin {
18
18
  }
19
19
  }
20
20
 
21
+ class TestSettingsLinkPlugin extends Plugin {
22
+ async load() {
23
+ this.app.pluginSettingsManager.addMenuItem({ key: 'shared-settings', title: 'Shared settings' });
24
+ this.app.pluginSettingsManager.addPageTabItem({
25
+ menuKey: 'shared-settings',
26
+ key: 'target',
27
+ title: 'Target settings',
28
+ Component: () => <div>Target settings page</div>,
29
+ });
30
+ this.app.pluginSettingsManager.setPluginSettingsLink('demo-plugin', 'shared-settings.target');
31
+ }
32
+ }
33
+
21
34
  type MockClientApplication = ReturnType<typeof createMockClient>;
22
35
 
23
36
  const renderApp = (app: MockClientApplication) => {
@@ -35,9 +48,9 @@ const waitForGetRequests = async (app: MockClientApplication, urls: string[]) =>
35
48
  );
36
49
  };
37
50
 
38
- const setupApp = (pmList: any[]) => {
51
+ const setupApp = (pmList: any[], plugins: Array<typeof Plugin> = []) => {
39
52
  const app = createMockClient({
40
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
53
+ plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ...plugins],
41
54
  router: { type: 'memory', initialEntries: ['/admin/settings/plugin-manager'] },
42
55
  });
43
56
 
@@ -174,4 +187,33 @@ describe('plugin-manager page', () => {
174
187
  expect(removeCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
175
188
  });
176
189
  });
190
+
191
+ it('shows Settings for a plugin linked to a different settings page', async () => {
192
+ const app = setupApp(
193
+ [
194
+ {
195
+ name: 'demo-plugin',
196
+ packageName: '@nocobase/demo-plugin',
197
+ displayName: 'Demo plugin',
198
+ description: 'A demo',
199
+ enabled: true,
200
+ builtIn: false,
201
+ removable: false,
202
+ version: '0.1.0',
203
+ isCompatible: true,
204
+ keywords: [],
205
+ },
206
+ ],
207
+ [TestSettingsLinkPlugin],
208
+ );
209
+
210
+ renderApp(app);
211
+ await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
212
+
213
+ const card = await screen.findByRole('button', { name: 'Demo plugin' });
214
+ const settingsLink = within(card.closest('.ant-card') as HTMLElement).getByText('Settings');
215
+ fireEvent.click(settingsLink);
216
+
217
+ expect(await screen.findByText('Target settings page')).toBeInTheDocument();
218
+ });
177
219
  });
@@ -14,7 +14,7 @@ import { message } from 'antd';
14
14
  import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
15
15
  import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
16
16
  import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
17
- import { matchSettingsRoute } from '../settings-center/utils';
17
+ import { matchSettingsRoute, sortTopLevelSettings } from '../settings-center/utils';
18
18
 
19
19
  class TestAclPlugin extends Plugin {
20
20
  async load() {
@@ -168,6 +168,12 @@ describe('settings center', () => {
168
168
  });
169
169
  });
170
170
 
171
+ it('should sort system-settings with other top-level settings by normal ordering', () => {
172
+ const settings = [{ name: 'system-settings' }, { name: 'api-keys' }, { name: 'backups' }] as any;
173
+
174
+ expect(sortTopLevelSettings(settings).map((item) => item.name)).toEqual(['api-keys', 'backups', 'system-settings']);
175
+ });
176
+
171
177
  it('should redirect /admin/settings to system-settings by default', async () => {
172
178
  const app = createMockClient({
173
179
  plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
@@ -354,6 +360,39 @@ describe('settings center', () => {
354
360
  expect(screen.getByRole('menuitem', { name: 'Menu ACL Demo' })).toBeInTheDocument();
355
361
  });
356
362
 
363
+ it('should allow the settings sidebar menu to scroll independently', async () => {
364
+ class ManySettingsPlugin extends Plugin {
365
+ async load() {
366
+ for (let index = 0; index < 30; index += 1) {
367
+ this.pluginSettingsManager.addMenuItem({
368
+ key: `scroll-demo-${index}`,
369
+ title: `Scroll demo ${index}`,
370
+ });
371
+ this.pluginSettingsManager.addPageTabItem({
372
+ menuKey: `scroll-demo-${index}`,
373
+ key: 'index',
374
+ title: `Scroll demo ${index}`,
375
+ Component: () => <div>{`Scroll demo page ${index}`}</div>,
376
+ });
377
+ }
378
+ }
379
+ }
380
+
381
+ const app = createMockClient({
382
+ plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ManySettingsPlugin],
383
+ router: { type: 'memory', initialEntries: ['/admin/settings/scroll-demo-29'] },
384
+ });
385
+ mockAdminRuntime(app);
386
+
387
+ await renderApp(app);
388
+ await waitForGetRequests(app, ['/auth:check', 'roles:check']);
389
+
390
+ expect(await screen.findByText('Scroll demo page 29')).toBeInTheDocument();
391
+
392
+ const sidebar = screen.getByRole('menuitem', { name: 'Scroll demo 29' }).closest('.ant-layout-sider');
393
+ expect(sidebar).toHaveStyle({ overflowY: 'auto' });
394
+ });
395
+
357
396
  it('should save system settings through systemSettings:put', async () => {
358
397
  const app = createMockClient({
359
398
  plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
@@ -204,6 +204,44 @@ function getDefaultV2AdminRedirectPath(app: AppLike) {
204
204
  return joinRootRelativePath(getV2EffectiveBasePath(app), '/admin');
205
205
  }
206
206
 
207
+ function isSafeRootRelativePath(value?: string | null) {
208
+ return !!value && value.startsWith('/') && !value.startsWith('//') && !value.startsWith('/\\');
209
+ }
210
+
211
+ function preserveTrailingSlash(originalPathname: string, value: string) {
212
+ if (originalPathname !== '/' && originalPathname.endsWith('/') && !value.endsWith('/')) {
213
+ return `${value}/`;
214
+ }
215
+ return value;
216
+ }
217
+
218
+ export function normalizeV2RedirectPath(app: AppLike, target?: string | null, fallbackPath = '/admin/') {
219
+ // In a v2 sub-app, publicPath can be `/v/` while the active router
220
+ // basename is `/v/apps/a/`. Redirects must resolve under the basename.
221
+ const basePath = trimTrailingSlashes(getV2EffectiveBasePath(app)) || '/';
222
+ const fallbackTarget = isSafeRootRelativePath(fallbackPath) ? fallbackPath : '/admin/';
223
+ const rawTarget = isSafeRootRelativePath(target) ? target : fallbackTarget;
224
+ let { pathname, search, hash } = splitPathLike(rawTarget);
225
+ let normalizedPathname = normalizePathname(pathname);
226
+
227
+ // Already under the current v2 runtime, e.g. `/v/apps/a/admin/`.
228
+ if (basePath === '/' || normalizedPathname === basePath || normalizedPathname.startsWith(`${basePath}/`)) {
229
+ return `${preserveTrailingSlash(pathname, normalizedPathname)}${normalizeSearch(search)}${normalizeHash(hash)}`;
230
+ }
231
+
232
+ const publicPath = trimTrailingSlashes(getV2PublicPath(app)) || '/';
233
+ // Under v2 publicPath but outside the current basename, e.g. `/v/admin/`
234
+ // inside `/v/apps/a/`; use fallback so it lands on `/v/apps/a/admin/`.
235
+ if (publicPath !== '/' && (normalizedPathname === publicPath || normalizedPathname.startsWith(`${publicPath}/`))) {
236
+ ({ pathname, search, hash } = splitPathLike(fallbackTarget));
237
+ normalizedPathname = normalizePathname(pathname);
238
+ }
239
+
240
+ // Basename-relative target, e.g. `/admin/`, becomes `/v/apps/a/admin/`.
241
+ const joinedPathname = preserveTrailingSlash(pathname, joinRootRelativePath(basePath, normalizedPathname));
242
+ return `${joinedPathname}${normalizeSearch(search)}${normalizeHash(hash)}`;
243
+ }
244
+
207
245
  /**
208
246
  * 将当前 v2 页面地址转换为根相对 redirect 路径。
209
247
  *
@@ -115,6 +115,7 @@ export abstract class CollectionFieldInterface {
115
115
  titleUsable?: boolean;
116
116
  usePathOptions?(field: any): any;
117
117
  hidden?: boolean;
118
+ creatable?: boolean;
118
119
 
119
120
  addComponentOption(componentOption: CollectionFieldInterfaceComponentOption) {
120
121
  if (!this.componentOptions) {
@@ -31,6 +31,7 @@ interface FieldInterfaceLike {
31
31
  title?: ReactNode;
32
32
  group?: string;
33
33
  order?: number;
34
+ creatable?: boolean;
34
35
  isAssociation?: boolean;
35
36
  supportDataSourceType?: string[];
36
37
  notSupportDataSourceType?: string[];
@@ -530,7 +530,7 @@ export function reverseFieldConfigureItems(): FieldConfigureItem[] {
530
530
  component: 'Input',
531
531
  required: true,
532
532
  description:
533
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
533
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
534
534
  hidden: ({ context, values }) => !context.showReverseFieldConfig && !get(values, 'autoCreateReverseField'),
535
535
  disabled: ({ context }) => !context.showReverseFieldConfig,
536
536
  },
@@ -132,7 +132,7 @@ fieldValidationConfigureRegistry.registerMany([
132
132
  { key: 'uri', label: 'URI', hasValue: false, params: [], paramsType: 'object' },
133
133
  {
134
134
  key: 'multiple',
135
- label: 'Multiple',
135
+ label: 'Multiple of',
136
136
  hasValue: true,
137
137
  params: [{ key: 'base', label: 'Base', componentType: 'inputNumber', required: true }],
138
138
  },
@@ -48,7 +48,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
48
48
  'x-decorator': 'FormItem',
49
49
  'x-component': 'Input',
50
50
  description:
51
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
51
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
52
52
  },
53
53
  };
54
54
  filterable = {
@@ -158,7 +158,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
158
158
  required: true,
159
159
  default: '{{ useNewId("f_") }}',
160
160
  description:
161
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
161
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
162
162
  'x-decorator': 'FormItem',
163
163
  'x-component': 'ForeignKey',
164
164
  'x-validator': 'uid',
@@ -192,7 +192,7 @@ export class M2MFieldInterface extends CollectionFieldInterface {
192
192
  required: true,
193
193
  default: '{{ useNewId("f_") }}',
194
194
  description:
195
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
195
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
196
196
  'x-decorator': 'FormItem',
197
197
  'x-component': 'ForeignKey',
198
198
  'x-validator': 'uid',
@@ -69,7 +69,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
69
69
  'x-decorator': 'FormItem',
70
70
  'x-component': 'Input',
71
71
  description:
72
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
72
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
73
73
  },
74
74
  type: relationshipType,
75
75
  grid: {
@@ -123,7 +123,7 @@ export class M2OFieldInterface extends CollectionFieldInterface {
123
123
  required: true,
124
124
  default: '{{ useNewId("f_") }}',
125
125
  description:
126
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
126
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
127
127
  'x-decorator': 'FormItem',
128
128
  'x-component': 'ForeignKey',
129
129
  'x-validator': 'uid',
@@ -42,7 +42,7 @@ export class NanoidFieldInterface extends CollectionFieldInterface {
42
42
  'x-component': 'Input',
43
43
  'x-disabled': '{{ !createOnly }}',
44
44
  description:
45
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
45
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
46
46
  },
47
47
  customAlphabet: {
48
48
  type: 'string',
@@ -68,7 +68,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
68
68
  'x-decorator': 'FormItem',
69
69
  'x-component': 'Input',
70
70
  description:
71
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
71
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
72
72
  },
73
73
  type: relationshipType,
74
74
  grid: {
@@ -135,7 +135,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
135
135
  required: true,
136
136
  default: '{{ useNewId("f_") }}',
137
137
  description:
138
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
138
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
139
139
  'x-decorator': 'FormItem',
140
140
  'x-component': 'ForeignKey',
141
141
  'x-validator': 'uid',
@@ -55,7 +55,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
55
55
  'x-decorator': 'FormItem',
56
56
  'x-component': 'Input',
57
57
  description:
58
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
58
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
59
59
  },
60
60
  type: relationshipType,
61
61
  grid: {
@@ -109,7 +109,7 @@ export class OBOFieldInterface extends CollectionFieldInterface {
109
109
  required: true,
110
110
  default: '{{ useNewId("f_") }}',
111
111
  description:
112
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
112
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
113
113
  'x-decorator': 'FormItem',
114
114
  'x-component': 'ForeignKey',
115
115
  'x-validator': 'uid',
@@ -55,7 +55,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
55
55
  'x-decorator': 'FormItem',
56
56
  'x-component': 'Input',
57
57
  description:
58
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
58
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
59
59
  },
60
60
  type: relationshipType,
61
61
  grid: {
@@ -123,7 +123,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
123
123
  required: true,
124
124
  default: '{{ useNewId("f_") }}',
125
125
  description:
126
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
126
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
127
127
  'x-decorator': 'FormItem',
128
128
  'x-component': 'ForeignKey',
129
129
  'x-validator': 'uid',
@@ -141,7 +141,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
141
141
  'x-validator': 'uid',
142
142
  'x-disabled': '{{ !showReverseFieldConfig }}',
143
143
  description:
144
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
144
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
145
145
  },
146
146
  },
147
147
  },
@@ -399,7 +399,7 @@ export const defaultProps = {
399
399
  'x-component': 'Input',
400
400
  'x-validator': 'uid',
401
401
  description:
402
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
402
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
403
403
  },
404
404
  };
405
405
 
@@ -46,7 +46,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
46
46
  'x-component': 'Input',
47
47
  'x-disabled': '{{ !createOnly }}',
48
48
  description:
49
- "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
49
+ "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.')}}",
50
50
  },
51
51
  autoFill,
52
52
  layout: {