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

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
@@ -8,26 +8,49 @@
8
8
  */
9
9
 
10
10
  import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
11
+ import { Button, Result } from 'antd';
11
12
  import React, { useEffect, useMemo, useRef, useState } from 'react';
12
- import { deviceType } from 'react-device-detect';
13
- import { useParams } from 'react-router-dom';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { useLocation, useNavigate, useParams } from 'react-router-dom';
15
+ import { getModernClientPrefix, stripModernClientPrefix } from '../../authRedirect';
14
16
  import { useApp } from '../../hooks/useApp';
15
- import { NocoBaseDesktopRouteType } from '../../flow-compat';
16
- import { resolveAdminRouteRuntimeTarget } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
17
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
18
+ import {
19
+ resolveAdminRouteRuntimeTarget,
20
+ toRouterNavigationPath,
21
+ } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
17
22
  import { getAdminLayoutModel, type AdminLayoutModel } from '../admin-shell/admin-layout/AdminLayoutModel';
18
23
  import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutModel';
19
24
  import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
20
25
  import { AppNotFound } from '../../components';
21
26
  import { useKeepAlive } from '../../components/KeepAlive';
27
+ import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
22
28
 
23
29
  type FlowRouteGuardState = {
30
+ pageUid?: string;
24
31
  pending: boolean;
25
32
  allowBridge: boolean;
26
33
  notFound: boolean;
34
+ legacyPageUnsupported?: boolean;
27
35
  };
28
36
 
29
37
  export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
30
38
 
39
+ type FlowRouteLayoutContext = {
40
+ authCheck?: boolean;
41
+ routePath?: string;
42
+ routeName?: string;
43
+ uid?: string;
44
+ };
45
+
46
+ type FlowRouteRepositoryLike = {
47
+ refreshAccessible?: () => Promise<unknown>;
48
+ isAccessibleLoaded?: () => boolean;
49
+ ensureAccessibleLoaded?: () => Promise<unknown>;
50
+ getRouteBySchemaUid?: (schemaUid: string) => NocoBaseDesktopRoute | undefined;
51
+ listAccessible?: () => NocoBaseDesktopRoute[];
52
+ };
53
+
31
54
  export type FlowRouteProps = {
32
55
  pageUid?: string;
33
56
  active?: boolean;
@@ -38,7 +61,7 @@ export type FlowRouteProps = {
38
61
  const getDefaultAdminLayoutModel = (flowEngine: FlowEngine) =>
39
62
  getAdminLayoutModel<AdminLayoutModel>(flowEngine, { required: true });
40
63
 
41
- const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
64
+ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: FlowRouteLayoutContext) => {
42
65
  const layout = contextLayout || flowEngine.context.layout;
43
66
 
44
67
  if (layout?.uid) {
@@ -48,7 +71,10 @@ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
48
71
  return getDefaultAdminLayoutModel(flowEngine);
49
72
  };
50
73
 
51
- const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: any): LegacyPageBehavior => {
74
+ const getDefaultLegacyPageBehavior = (
75
+ flowEngine: FlowEngine,
76
+ contextLayout?: FlowRouteLayoutContext,
77
+ ): LegacyPageBehavior => {
52
78
  const layout = contextLayout || flowEngine.context.layout;
53
79
 
54
80
  if (layout?.routeName && layout.routeName !== 'admin') {
@@ -58,6 +84,46 @@ const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: an
58
84
  return 'redirect';
59
85
  };
60
86
 
87
+ const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
88
+
89
+ const findAccessibleRouteByIdentity = (
90
+ routes: NocoBaseDesktopRoute[] | undefined,
91
+ pageUid: string,
92
+ ): NocoBaseDesktopRoute | undefined => {
93
+ if (!Array.isArray(routes)) {
94
+ return undefined;
95
+ }
96
+
97
+ for (const route of routes) {
98
+ if (
99
+ (route.type !== NocoBaseDesktopRouteType.group && route.schemaUid === pageUid) ||
100
+ (route.type === NocoBaseDesktopRouteType.group && route.id != null && String(route.id) === pageUid)
101
+ ) {
102
+ return route;
103
+ }
104
+
105
+ const matched = findAccessibleRouteByIdentity(route.children, pageUid);
106
+ if (matched) {
107
+ return matched;
108
+ }
109
+ }
110
+
111
+ return undefined;
112
+ };
113
+
114
+ const getAccessibleRouteByPageUid = (
115
+ routeRepository: FlowRouteRepositoryLike | undefined,
116
+ pageUid: string,
117
+ ): NocoBaseDesktopRoute | undefined => {
118
+ const routeBySchemaUid = routeRepository?.getRouteBySchemaUid?.(pageUid);
119
+
120
+ if (routeBySchemaUid && routeBySchemaUid.type !== NocoBaseDesktopRouteType.group) {
121
+ return routeBySchemaUid;
122
+ }
123
+
124
+ return findAccessibleRouteByIdentity(routeRepository?.listAccessible?.(), pageUid);
125
+ };
126
+
61
127
  const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
62
128
  if (flowEngine.getModel(pageUid)) {
63
129
  return true;
@@ -94,31 +160,9 @@ const BridgeFlowRoute = ({
94
160
  const layoutContentRef = useRef<HTMLDivElement>(null);
95
161
 
96
162
  useEffect(() => {
97
- flowEngine.context.defineProperty('deviceType', {
98
- get: () => (deviceType === 'browser' ? 'computer' : deviceType),
99
- cache: false,
100
- meta: {
101
- type: 'string',
102
- title: flowEngine.translate('Current device type'),
103
- interface: 'select',
104
- uiSchema: {
105
- enum: [
106
- { label: flowEngine.translate('Computer'), value: 'computer' },
107
- { label: flowEngine.translate('Mobile'), value: 'mobile' },
108
- { label: flowEngine.translate('Tablet'), value: 'tablet' },
109
- { label: flowEngine.translate('SmartTv'), value: 'smarttv' },
110
- { label: flowEngine.translate('Console'), value: 'console' },
111
- { label: flowEngine.translate('Wearable'), value: 'wearable' },
112
- { label: flowEngine.translate('Embedded'), value: 'embedded' },
113
- ],
114
- 'x-component': 'Select',
115
- },
116
- },
117
- info: {
118
- description: 'Current device type (computer/mobile/tablet/...).',
119
- detail: 'string',
120
- },
121
- });
163
+ if (!flowEngine.context.getPropertyOptions('deviceType')) {
164
+ registerDeviceTypeContext(flowEngine);
165
+ }
122
166
  }, [flowEngine]);
123
167
 
124
168
  useLayoutRoutePage({
@@ -133,11 +177,58 @@ const BridgeFlowRoute = ({
133
177
  return <div ref={layoutContentRef} />;
134
178
  };
135
179
 
180
+ type RouteLocation = {
181
+ pathname: string;
182
+ search: string;
183
+ hash: string;
184
+ };
185
+
186
+ const getLegacyPageHref = (app: { getPublicPath: () => string }, location: RouteLocation) => {
187
+ const modernPublicPath = app.getPublicPath();
188
+ const browserLocationMatchesPublicPath = window.location.pathname.startsWith(modernPublicPath);
189
+ const currentLocation = browserLocationMatchesPublicPath ? window.location : location;
190
+ const pathWithinModernClient = currentLocation.pathname.startsWith(modernPublicPath)
191
+ ? currentLocation.pathname.slice(modernPublicPath.length)
192
+ : currentLocation.pathname.replace(/^\/+/, '');
193
+ return `${stripModernClientPrefix(modernPublicPath)}${pathWithinModernClient}${currentLocation.search}${
194
+ currentLocation.hash
195
+ }`;
196
+ };
197
+
198
+ const LegacyPageUnsupported = ({
199
+ app,
200
+ location,
201
+ }: {
202
+ app: { getPublicPath: () => string };
203
+ location: RouteLocation;
204
+ }) => {
205
+ const { t } = useTranslation();
206
+ const modernClientPath = `/${getModernClientPrefix()}/`;
207
+ const withModernClientPath = (message: string) => message.replaceAll('{{modernClientPath}}', modernClientPath);
208
+
209
+ return (
210
+ <Result
211
+ status="warning"
212
+ title={withModernClientPath(t('This page is not supported in the {{modernClientPath}} branch'))}
213
+ subTitle={withModernClientPath(
214
+ t(
215
+ 'The {{modernClientPath}} branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
216
+ ),
217
+ )}
218
+ extra={
219
+ <Button href={getLegacyPageHref(app, location)} type="primary">
220
+ {t('Open from the original entry')}
221
+ </Button>
222
+ }
223
+ />
224
+ );
225
+ };
226
+
136
227
  /**
137
228
  * 管理后台动态页面路由组件。
138
229
  *
139
- * 负责读取当前路由页面 UID,补充运行时设备变量,
140
- * 并把页面生命周期桥接到 AdminLayout host model。
230
+ * 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
231
+ * 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
141
232
  *
142
233
  * @example
143
234
  * ```tsx
@@ -148,20 +239,52 @@ const BridgeFlowRoute = ({
148
239
  * @throws {Error} 当缺少 `route.params.name` 时抛出异常
149
240
  */
150
241
  const FlowRoute = (props: FlowRouteProps = {}) => {
242
+ const {
243
+ active,
244
+ getLayoutModel: getLayoutModelProp,
245
+ legacyPageBehavior: legacyPageBehaviorProp,
246
+ pageUid: pageUidProp,
247
+ } = props;
151
248
  const flowEngine = useFlowEngine();
152
- const flowContext = useFlowContext<any>();
249
+ const flowContext = useFlowContext<{ layout?: FlowRouteLayoutContext }>();
153
250
  const contextLayout = flowContext?.layout;
251
+ const propsLayoutModel = useMemo(() => getLayoutModelProp?.(flowEngine), [flowEngine, getLayoutModelProp]);
252
+ const rawRouteLayout = contextLayout || propsLayoutModel?.layout;
253
+ const routeLayoutUid = rawRouteLayout?.uid;
254
+ const routeLayoutRouteName = rawRouteLayout?.routeName;
255
+ const routeLayoutRoutePath = rawRouteLayout?.routePath;
256
+ const routeLayoutAuthCheck = rawRouteLayout?.authCheck;
257
+ const routeLayout = useMemo(() => {
258
+ if (
259
+ !routeLayoutUid &&
260
+ !routeLayoutRouteName &&
261
+ !routeLayoutRoutePath &&
262
+ typeof routeLayoutAuthCheck === 'undefined'
263
+ ) {
264
+ return undefined;
265
+ }
266
+ return {
267
+ authCheck: routeLayoutAuthCheck,
268
+ routeName: routeLayoutRouteName,
269
+ routePath: routeLayoutRoutePath,
270
+ uid: routeLayoutUid,
271
+ };
272
+ }, [routeLayoutAuthCheck, routeLayoutRouteName, routeLayoutRoutePath, routeLayoutUid]);
154
273
  const getLayoutModel = useMemo(
155
- () => props.getLayoutModel || ((engine: FlowEngine) => getDefaultLayoutModel(engine, contextLayout)),
156
- [contextLayout, props.getLayoutModel],
274
+ () => getLayoutModelProp || ((engine: FlowEngine) => getDefaultLayoutModel(engine, routeLayout)),
275
+ [getLayoutModelProp, routeLayout],
157
276
  );
158
- const legacyPageBehavior = props.legacyPageBehavior || getDefaultLegacyPageBehavior(flowEngine, contextLayout);
277
+ const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
159
278
  const app = useApp();
160
- const routeRepository = flowEngine.context.routeRepository;
279
+ const location = useLocation();
280
+ const navigate = useNavigate();
281
+ const routeRepository = flowEngine.context.routeRepository as FlowRouteRepositoryLike | undefined;
161
282
  const params = useParams();
162
- const pageUid = props.pageUid || params?.name;
283
+ const pageUid = pageUidProp || params?.name;
284
+ const hasNestedRoutePath = typeof params?.tabUid !== 'undefined' || typeof params?.['*'] !== 'undefined';
163
285
  const skipRouteRepositoryCheck = !routeRepository;
164
286
  const [guardState, setGuardState] = useState<FlowRouteGuardState>({
287
+ pageUid: undefined,
165
288
  pending: true,
166
289
  allowBridge: false,
167
290
  notFound: false,
@@ -173,19 +296,24 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
173
296
  throw new Error('[NocoBase] FlowRoute requires pageUid or route.params.name.');
174
297
  }
175
298
 
299
+ useEffect(() => {
300
+ replaceTriggeredRef.current = false;
301
+ }, [location.hash, location.pathname, location.search, pageUid]);
302
+
176
303
  useEffect(() => {
177
304
  let active = true;
178
305
  const requestId = ++requestIdRef.current;
306
+ const requiresAccessibleRoute = shouldRequireAccessibleRoute(routeLayout);
179
307
 
180
308
  const run = async () => {
181
- setGuardState({ pending: true, allowBridge: false, notFound: false });
309
+ setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
182
310
 
183
- if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
311
+ if (requiresAccessibleRoute && !skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
184
312
  try {
185
313
  await routeRepository?.ensureAccessibleLoaded?.();
186
314
  } catch (_error) {
187
315
  if (active && requestId === requestIdRef.current) {
188
- setGuardState({ pending: false, allowBridge: true, notFound: false });
316
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
189
317
  }
190
318
  return;
191
319
  }
@@ -195,23 +323,67 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
195
323
  return;
196
324
  }
197
325
 
198
- const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
326
+ const route =
327
+ skipRouteRepositoryCheck || !requiresAccessibleRoute
328
+ ? undefined
329
+ : getAccessibleRouteByPageUid(routeRepository, pageUid);
330
+ if (!route && !skipRouteRepositoryCheck && requiresAccessibleRoute) {
331
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
332
+ return;
333
+ }
334
+
199
335
  if (!route && legacyPageBehavior === 'notFound') {
200
336
  const flowModelExists = await hasFlowModel(flowEngine, pageUid);
201
337
  if (active && requestId === requestIdRef.current) {
202
- setGuardState({ pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
338
+ setGuardState({ pageUid, pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
203
339
  }
204
340
  return;
205
341
  }
206
342
 
343
+ if (route?.type === NocoBaseDesktopRouteType.group) {
344
+ if (hasNestedRoutePath) {
345
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
346
+ return;
347
+ }
348
+
349
+ const target = resolveAdminRouteRuntimeTarget({
350
+ app,
351
+ route,
352
+ layout: routeLayout,
353
+ });
354
+
355
+ if (target.reason === 'emptyGroup') {
356
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
357
+ return;
358
+ }
359
+
360
+ if (target.runtimePath) {
361
+ if (replaceTriggeredRef.current) {
362
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
363
+ return;
364
+ }
365
+
366
+ replaceTriggeredRef.current = true;
367
+ if (target.navigationMode === 'document') {
368
+ window.location.replace(target.runtimePath);
369
+ return;
370
+ }
371
+ navigate(toRouterNavigationPath(target.runtimePath, app.router?.getBasename?.()), { replace: true });
372
+ return;
373
+ }
374
+
375
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
376
+ return;
377
+ }
378
+
207
379
  if (route?.type === NocoBaseDesktopRouteType.page) {
208
380
  if (legacyPageBehavior === 'notFound') {
209
- setGuardState({ pending: false, allowBridge: false, notFound: true });
381
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
210
382
  return;
211
383
  }
212
384
 
213
385
  if (legacyPageBehavior === 'bridge') {
214
- setGuardState({ pending: false, allowBridge: true, notFound: false });
386
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
215
387
  return;
216
388
  }
217
389
 
@@ -234,26 +406,42 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
234
406
 
235
407
  if (target.reason === 'unsupportedV2Runtime') {
236
408
  if (active && requestId === requestIdRef.current) {
237
- setGuardState({ pending: false, allowBridge: false, notFound: true });
409
+ setGuardState({
410
+ pageUid,
411
+ pending: false,
412
+ allowBridge: false,
413
+ notFound: false,
414
+ legacyPageUnsupported: true,
415
+ });
238
416
  }
239
417
  return;
240
418
  }
241
419
  }
242
420
 
243
421
  if (active && requestId === requestIdRef.current) {
244
- setGuardState({ pending: false, allowBridge: true, notFound: false });
422
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
245
423
  }
246
424
  };
247
425
 
248
- void run();
426
+ run();
249
427
 
250
428
  return () => {
251
429
  active = false;
252
430
  };
253
- }, [app, flowEngine, legacyPageBehavior, pageUid, routeRepository, skipRouteRepositoryCheck]);
431
+ }, [
432
+ app,
433
+ flowEngine,
434
+ hasNestedRoutePath,
435
+ legacyPageBehavior,
436
+ navigate,
437
+ pageUid,
438
+ routeLayout,
439
+ routeRepository,
440
+ skipRouteRepositoryCheck,
441
+ ]);
254
442
 
255
443
  const content = useMemo(() => {
256
- if (guardState.pending) {
444
+ if (guardState.pageUid !== pageUid || guardState.pending) {
257
445
  return null;
258
446
  }
259
447
 
@@ -261,12 +449,27 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
261
449
  return <AppNotFound />;
262
450
  }
263
451
 
452
+ if (guardState.legacyPageUnsupported) {
453
+ return <LegacyPageUnsupported app={app} location={location} />;
454
+ }
455
+
264
456
  if (!guardState.allowBridge) {
265
457
  return null;
266
458
  }
267
459
 
268
- return <BridgeFlowRoute pageUid={pageUid} active={props.active} getLayoutModel={getLayoutModel} />;
269
- }, [getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid, props.active]);
460
+ return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
461
+ }, [
462
+ active,
463
+ app,
464
+ getLayoutModel,
465
+ guardState.allowBridge,
466
+ guardState.legacyPageUnsupported,
467
+ guardState.notFound,
468
+ guardState.pageUid,
469
+ guardState.pending,
470
+ location,
471
+ pageUid,
472
+ ]);
270
473
 
271
474
  return content;
272
475
  };
@@ -15,6 +15,7 @@ export interface RunJSValueEditorProps {
15
15
  t?: (key: string) => string;
16
16
  value?: unknown;
17
17
  onChange?: (value: RunJSValue) => void;
18
+ disabled?: boolean;
18
19
  height?: string;
19
20
  scene?: string;
20
21
  containerStyle?: React.CSSProperties;
@@ -25,6 +26,7 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
25
26
  t,
26
27
  value,
27
28
  onChange,
29
+ disabled,
28
30
  height = '200px',
29
31
  scene = 'formValue',
30
32
  containerStyle = { flex: 1, minWidth: 0 },
@@ -38,9 +40,15 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
38
40
  <div style={containerStyle}>
39
41
  <CodeEditor
40
42
  value={current.code}
41
- onChange={(code) => onChange?.({ ...current, code })}
43
+ onChange={(code) => {
44
+ if (disabled) {
45
+ return;
46
+ }
47
+ onChange?.({ ...current, code });
48
+ }}
42
49
  version={current.version}
43
50
  height={height}
51
+ readonly={disabled}
44
52
  enableLinter
45
53
  placeholder={placeholderText}
46
54
  scene={scene}
@@ -10,19 +10,24 @@
10
10
  import React from 'react';
11
11
  import { beforeEach, describe, expect, it, vi } from 'vitest';
12
12
  import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
13
- import { ActionScene, FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID } from '@nocobase/flow-engine';
13
+ import { ActionScene, FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID, useFlowContext } from '@nocobase/flow-engine';
14
14
  import { DynamicFlowsIcon } from '../DynamicFlowsIcon';
15
15
 
16
16
  const mockState = vi.hoisted(() => ({
17
17
  capturedSelectProps: [] as any[],
18
+ capturedTabsProps: [] as any[],
18
19
  flowContextValue: undefined as any,
19
20
  }));
20
21
 
21
22
  vi.mock('@nocobase/flow-engine', async (importOriginal) => {
22
23
  const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
24
+ const React = await import('react');
23
25
  return {
24
26
  ...actual,
25
- useFlowContext: () => mockState.flowContextValue,
27
+ useFlowContext: () => {
28
+ const context = React.useContext(actual.FlowReactContext);
29
+ return context?.view ? context : mockState.flowContextValue;
30
+ },
26
31
  };
27
32
  });
28
33
 
@@ -62,6 +67,33 @@ vi.mock('antd', async (importOriginal) => {
62
67
  return <div data-testid={String(props.placeholder || 'select')} />;
63
68
  },
64
69
  Space: ({ children }: any) => <div>{children}</div>,
70
+ Tabs: (props: any) => {
71
+ mockState.capturedTabsProps.push(props);
72
+ const { items, activeKey, onChange, tabBarStyle } = props;
73
+ const currentKey = activeKey || items?.[0]?.key;
74
+ return (
75
+ <div data-testid="dynamic-flow-source-tabs">
76
+ <div role="tablist" style={tabBarStyle}>
77
+ {items?.map((item: any) => (
78
+ <button
79
+ key={item.key}
80
+ role="tab"
81
+ aria-selected={item.key === currentKey}
82
+ type="button"
83
+ onClick={() => onChange?.(item.key)}
84
+ >
85
+ {item.label}
86
+ </button>
87
+ ))}
88
+ </div>
89
+ {items?.map((item: any) => (
90
+ <div key={item.key} hidden={item.key !== currentKey}>
91
+ {item.children}
92
+ </div>
93
+ ))}
94
+ </div>
95
+ );
96
+ },
65
97
  Tooltip: ({ children }: any) => <>{children}</>,
66
98
  };
67
99
  });
@@ -83,6 +115,8 @@ const openDynamicFlowsEditor = async (model: FlowModel) => {
83
115
  expect(trigger).not.toBeNull();
84
116
  await userEvent.click(trigger as Element);
85
117
 
118
+ await waitFor(() => expect(model.context.viewer.embed).toHaveBeenCalled());
119
+
86
120
  const embedCall = (model.context.viewer.embed as any).mock.calls.at(-1)?.[0];
87
121
  expect(embedCall?.content).toBeTruthy();
88
122
 
@@ -163,6 +197,38 @@ const createModel = (options: { preventClose?: boolean; hiddenClose?: boolean; s
163
197
  return model;
164
198
  };
165
199
 
200
+ const createPeerModel = (model: FlowModel, uid: string, saveStepParams = vi.fn(async () => undefined)) => {
201
+ const PeerModel = model.constructor as typeof FlowModel;
202
+ const peer = new PeerModel({
203
+ uid,
204
+ use: PeerModel,
205
+ flowEngine: model.flowEngine,
206
+ stepParams: {},
207
+ subModels: {},
208
+ flowRegistry: {
209
+ flow1: {
210
+ title: 'Template event flow',
211
+ on: 'click',
212
+ steps: {
213
+ notifyStep: {
214
+ use: 'notify',
215
+ },
216
+ },
217
+ },
218
+ },
219
+ } as any);
220
+
221
+ peer.context.defineProperty('message', {
222
+ value: {
223
+ error: vi.fn(),
224
+ success: vi.fn(),
225
+ },
226
+ });
227
+ vi.spyOn(peer, 'saveStepParams').mockImplementation(saveStepParams);
228
+
229
+ return peer;
230
+ };
231
+
166
232
  const createView = () => {
167
233
  let destroyed = false;
168
234
  let closingPromise: Promise<boolean | void> | undefined;
@@ -189,6 +255,9 @@ const createView = () => {
189
255
  destroyed = true;
190
256
  }),
191
257
  beforeClose: undefined as any,
258
+ inputArgs: {
259
+ pageActive: true,
260
+ },
192
261
  };
193
262
  return view;
194
263
  };
@@ -196,6 +265,7 @@ const createView = () => {
196
265
  describe('DynamicFlowsIcon', () => {
197
266
  beforeEach(() => {
198
267
  mockState.capturedSelectProps.length = 0;
268
+ mockState.capturedTabsProps.length = 0;
199
269
  mockState.flowContextValue = {
200
270
  modal: {
201
271
  confirm: vi.fn(),
@@ -377,4 +447,80 @@ describe('DynamicFlowsIcon', () => {
377
447
  expect(view.destroy).toHaveBeenCalledTimes(1);
378
448
  expect(model.context.message.success).toHaveBeenCalledWith('Configuration saved');
379
449
  });
450
+
451
+ it('renders source tabs and closes the editor when saving one source', async () => {
452
+ const model = createModel();
453
+ const targetSaveStepParams = vi.fn(async () => undefined);
454
+ const target = createPeerModel(model, 'target-model', targetSaveStepParams);
455
+
456
+ model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
457
+ key: 'test-source-provider',
458
+ getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
459
+ });
460
+
461
+ const { view } = await openDynamicFlowsEditor(model);
462
+
463
+ expect(screen.getByRole('tab', { name: 'Current block' })).toHaveAttribute('aria-selected', 'true');
464
+ expect(mockState.capturedTabsProps.at(-1)?.tabBarStyle?.paddingLeft).toBeGreaterThan(0);
465
+ await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
466
+
467
+ await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
468
+ await userEvent.click(screen.getByRole('button', { name: 'Save' }));
469
+
470
+ expect(targetSaveStepParams).toHaveBeenCalledTimes(1);
471
+ expect(target.flowRegistry.getFlows().size).toBe(2);
472
+ expect(view.destroy).toHaveBeenCalledTimes(1);
473
+ expect(target.context.message.success).toHaveBeenCalledWith('Configuration saved');
474
+ });
475
+
476
+ it('saves all dirty sources without discard confirmation', async () => {
477
+ const saveStepParams = vi.fn(async () => undefined);
478
+ const model = createModel({ saveStepParams });
479
+ const targetSaveStepParams = vi.fn(async () => undefined);
480
+ const target = createPeerModel(model, 'target-model', targetSaveStepParams);
481
+ mockState.flowContextValue.modal.confirm.mockResolvedValue(false);
482
+
483
+ model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
484
+ key: 'test-source-unsaved-provider',
485
+ getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
486
+ });
487
+
488
+ const { view } = await openDynamicFlowsEditor(model);
489
+
490
+ await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
491
+ await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
492
+ await userEvent.click(screen.getByRole('tab', { name: 'Current block' }));
493
+ await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
494
+ await userEvent.click(screen.getByRole('button', { name: 'Save' }));
495
+
496
+ expect(saveStepParams).toHaveBeenCalledTimes(1);
497
+ expect(targetSaveStepParams).toHaveBeenCalledTimes(1);
498
+ expect(mockState.flowContextValue.modal.confirm).not.toHaveBeenCalled();
499
+ expect(view.close).toHaveBeenCalledTimes(1);
500
+ expect(view.destroy).toHaveBeenCalledTimes(1);
501
+ expect(model.flowRegistry.getFlows().size).toBe(2);
502
+ expect(target.flowRegistry.getFlows().size).toBe(2);
503
+ });
504
+
505
+ it('provides the active source model context inside source tabs', async () => {
506
+ const model = createModel();
507
+ const target = createPeerModel(model, 'target-model');
508
+ const seenModelUids: string[] = [];
509
+ const ContextProbe = () => {
510
+ const ctx = useFlowContext<any>();
511
+ seenModelUids.push(ctx?.model?.uid);
512
+ return null;
513
+ };
514
+ model.flowEngine.flowSettings.renderStepForm = vi.fn(() => <ContextProbe />) as any;
515
+
516
+ model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
517
+ key: 'test-source-context-provider',
518
+ getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
519
+ });
520
+
521
+ await openDynamicFlowsEditor(model);
522
+ await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
523
+
524
+ await waitFor(() => expect(seenModelUids).toContain('target-model'));
525
+ });
380
526
  });