@nocobase/flow-engine 2.0.0-alpha.2

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 (597) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +30 -0
  3. package/lib/ContextPathProxy.d.ts +17 -0
  4. package/lib/ContextPathProxy.js +65 -0
  5. package/lib/ElementProxy.d.ts +17 -0
  6. package/lib/ElementProxy.js +93 -0
  7. package/lib/FlowContextProvider.d.ts +24 -0
  8. package/lib/FlowContextProvider.js +82 -0
  9. package/lib/FlowDefinition.d.ts +423 -0
  10. package/lib/FlowDefinition.js +257 -0
  11. package/lib/JSRunner.d.ts +32 -0
  12. package/lib/JSRunner.js +95 -0
  13. package/lib/ReactView.d.ts +20 -0
  14. package/lib/ReactView.js +120 -0
  15. package/lib/ViewScopedFlowEngine.d.ts +23 -0
  16. package/lib/ViewScopedFlowEngine.js +81 -0
  17. package/lib/acl/Acl.d.ts +31 -0
  18. package/lib/acl/Acl.js +115 -0
  19. package/lib/action-registry/BaseActionRegistry.d.ts +23 -0
  20. package/lib/action-registry/BaseActionRegistry.js +57 -0
  21. package/lib/action-registry/EngineActionRegistry.d.ts +20 -0
  22. package/lib/action-registry/EngineActionRegistry.js +47 -0
  23. package/lib/action-registry/ModelActionRegistry.d.ts +34 -0
  24. package/lib/action-registry/ModelActionRegistry.js +79 -0
  25. package/lib/components/DynamicFlowsEditor.d.ts +17 -0
  26. package/lib/components/DynamicFlowsEditor.js +49 -0
  27. package/lib/components/FieldModelRenderer.d.ts +10 -0
  28. package/lib/components/FieldModelRenderer.js +94 -0
  29. package/lib/components/FlowContextSelector.d.ts +11 -0
  30. package/lib/components/FlowContextSelector.js +221 -0
  31. package/lib/components/FlowErrorFallback.d.ts +25 -0
  32. package/lib/components/FlowErrorFallback.js +264 -0
  33. package/lib/components/FlowModelRenderer.d.ts +64 -0
  34. package/lib/components/FlowModelRenderer.js +254 -0
  35. package/lib/components/FormItem.d.ts +18 -0
  36. package/lib/components/FormItem.js +147 -0
  37. package/lib/components/common/FlowSettingsButton.d.ts +11 -0
  38. package/lib/components/common/FlowSettingsButton.js +66 -0
  39. package/lib/components/common/index.d.ts +9 -0
  40. package/lib/components/common/index.js +30 -0
  41. package/lib/components/common/withFlowDesignMode.d.ts +26 -0
  42. package/lib/components/common/withFlowDesignMode.js +61 -0
  43. package/lib/components/dnd/getMousePositionOnElement.d.ts +50 -0
  44. package/lib/components/dnd/getMousePositionOnElement.js +95 -0
  45. package/lib/components/dnd/index.d.ts +24 -0
  46. package/lib/components/dnd/index.js +164 -0
  47. package/lib/components/dnd/moveBlock.d.ts +33 -0
  48. package/lib/components/dnd/moveBlock.js +302 -0
  49. package/lib/components/index.d.ts +18 -0
  50. package/lib/components/index.js +48 -0
  51. package/lib/components/settings/independents/dropdown/FlowsDropdownButton.d.ts +46 -0
  52. package/lib/components/settings/independents/dropdown/FlowsDropdownButton.js +225 -0
  53. package/lib/components/settings/independents/dropdown/index.d.ts +9 -0
  54. package/lib/components/settings/independents/dropdown/index.js +30 -0
  55. package/lib/components/settings/independents/index.d.ts +1 -0
  56. package/lib/components/settings/independents/index.js +30 -0
  57. package/lib/components/settings/index.d.ts +10 -0
  58. package/lib/components/settings/index.js +32 -0
  59. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +24 -0
  60. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +501 -0
  61. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.d.ts +45 -0
  62. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +231 -0
  63. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +111 -0
  64. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +484 -0
  65. package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.d.ts +26 -0
  66. package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.js +342 -0
  67. package/lib/components/settings/wrappers/contextual/StepSettings.d.ts +23 -0
  68. package/lib/components/settings/wrappers/contextual/StepSettings.js +110 -0
  69. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.d.ts +20 -0
  70. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +206 -0
  71. package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.d.ts +20 -0
  72. package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.js +47 -0
  73. package/lib/components/settings/wrappers/contextual/index.d.ts +14 -0
  74. package/lib/components/settings/wrappers/contextual/index.js +40 -0
  75. package/lib/components/settings/wrappers/embedded/FlowSettings.d.ts +33 -0
  76. package/lib/components/settings/wrappers/embedded/FlowSettings.js +207 -0
  77. package/lib/components/settings/wrappers/embedded/FlowsSettings.d.ts +41 -0
  78. package/lib/components/settings/wrappers/embedded/FlowsSettings.js +84 -0
  79. package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.d.ts +16 -0
  80. package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.js +88 -0
  81. package/lib/components/settings/wrappers/embedded/index.d.ts +10 -0
  82. package/lib/components/settings/wrappers/embedded/index.js +32 -0
  83. package/lib/components/settings/wrappers/index.d.ts +2 -0
  84. package/lib/components/settings/wrappers/index.js +32 -0
  85. package/lib/components/subModel/AddSubModelButton.d.ts +62 -0
  86. package/lib/components/subModel/AddSubModelButton.js +415 -0
  87. package/lib/components/subModel/LazyDropdown.d.ts +55 -0
  88. package/lib/components/subModel/LazyDropdown.js +524 -0
  89. package/lib/components/subModel/index.d.ts +10 -0
  90. package/lib/components/subModel/index.js +32 -0
  91. package/lib/components/subModel/utils.d.ts +34 -0
  92. package/lib/components/subModel/utils.js +287 -0
  93. package/lib/components/variables/InlineVariableTag.d.ts +21 -0
  94. package/lib/components/variables/InlineVariableTag.js +123 -0
  95. package/lib/components/variables/SlateVariableEditor.d.ts +46 -0
  96. package/lib/components/variables/SlateVariableEditor.js +302 -0
  97. package/lib/components/variables/VariableInput.d.ts +11 -0
  98. package/lib/components/variables/VariableInput.js +322 -0
  99. package/lib/components/variables/VariableTag.d.ts +11 -0
  100. package/lib/components/variables/VariableTag.js +148 -0
  101. package/lib/components/variables/VariableTrigger.d.ts +20 -0
  102. package/lib/components/variables/VariableTrigger.js +136 -0
  103. package/lib/components/variables/index.d.ts +15 -0
  104. package/lib/components/variables/index.js +53 -0
  105. package/lib/components/variables/types.d.ts +93 -0
  106. package/lib/components/variables/types.js +24 -0
  107. package/lib/components/variables/useResolvedMetaTree.d.ts +19 -0
  108. package/lib/components/variables/useResolvedMetaTree.js +91 -0
  109. package/lib/components/variables/utils.d.ts +22 -0
  110. package/lib/components/variables/utils.js +177 -0
  111. package/lib/data-source/index.d.ts +180 -0
  112. package/lib/data-source/index.js +733 -0
  113. package/lib/data-source/jioToJoiSchema.d.ts +19 -0
  114. package/lib/data-source/jioToJoiSchema.js +114 -0
  115. package/lib/decorators/index.d.ts +9 -0
  116. package/lib/decorators/index.js +36 -0
  117. package/lib/decorators/largeField.d.ts +9 -0
  118. package/lib/decorators/largeField.js +42 -0
  119. package/lib/emitter.d.ts +16 -0
  120. package/lib/emitter.js +58 -0
  121. package/lib/event-registry/BaseEventRegistry.d.ts +22 -0
  122. package/lib/event-registry/BaseEventRegistry.js +57 -0
  123. package/lib/event-registry/EngineEventRegistry.d.ts +19 -0
  124. package/lib/event-registry/EngineEventRegistry.js +47 -0
  125. package/lib/event-registry/ModelEventRegistry.d.ts +33 -0
  126. package/lib/event-registry/ModelEventRegistry.js +79 -0
  127. package/lib/executor/FlowExecutor.d.ts +26 -0
  128. package/lib/executor/FlowExecutor.js +262 -0
  129. package/lib/flow-registry/BaseFlowRegistry.d.ts +46 -0
  130. package/lib/flow-registry/BaseFlowRegistry.js +86 -0
  131. package/lib/flow-registry/GlobalFlowRegistry.d.ts +22 -0
  132. package/lib/flow-registry/GlobalFlowRegistry.js +95 -0
  133. package/lib/flow-registry/InstanceFlowRegistry.d.ts +21 -0
  134. package/lib/flow-registry/InstanceFlowRegistry.js +59 -0
  135. package/lib/flow-registry/index.d.ts +11 -0
  136. package/lib/flow-registry/index.js +34 -0
  137. package/lib/flowContext.d.ts +215 -0
  138. package/lib/flowContext.js +1266 -0
  139. package/lib/flowEngine.d.ts +340 -0
  140. package/lib/flowEngine.js +781 -0
  141. package/lib/flowI18n.d.ts +46 -0
  142. package/lib/flowI18n.js +117 -0
  143. package/lib/flowSettings.d.ts +266 -0
  144. package/lib/flowSettings.js +850 -0
  145. package/lib/hooks/index.d.ts +14 -0
  146. package/lib/hooks/index.js +40 -0
  147. package/lib/hooks/useApplyAutoFlows.d.ts +21 -0
  148. package/lib/hooks/useApplyAutoFlows.js +62 -0
  149. package/lib/hooks/useFlowModel.d.ts +29 -0
  150. package/lib/hooks/useFlowModel.js +72 -0
  151. package/lib/hooks/useFlowModelById.d.ts +11 -0
  152. package/lib/hooks/useFlowModelById.js +61 -0
  153. package/lib/hooks/useFlowSettingsContext.d.ts +20 -0
  154. package/lib/hooks/useFlowSettingsContext.js +61 -0
  155. package/lib/hooks/useFlowStep.d.ts +17 -0
  156. package/lib/hooks/useFlowStep.js +56 -0
  157. package/lib/hooks/useNiceDropdownMaxHeight.d.ts +13 -0
  158. package/lib/hooks/useNiceDropdownMaxHeight.js +52 -0
  159. package/lib/index.d.ts +27 -0
  160. package/lib/index.js +73 -0
  161. package/lib/locale/en-US.json +61 -0
  162. package/lib/locale/index.d.ts +141 -0
  163. package/lib/locale/index.js +70 -0
  164. package/lib/locale/zh-CN.json +61 -0
  165. package/lib/models/CollectionFieldModel.d.ts +50 -0
  166. package/lib/models/CollectionFieldModel.js +242 -0
  167. package/lib/models/DisplayItemModel.d.ts +12 -0
  168. package/lib/models/DisplayItemModel.js +41 -0
  169. package/lib/models/EditableItemModel.d.ts +12 -0
  170. package/lib/models/EditableItemModel.js +41 -0
  171. package/lib/models/FilterableItemModel.d.ts +12 -0
  172. package/lib/models/FilterableItemModel.js +41 -0
  173. package/lib/models/flowModel.d.ts +344 -0
  174. package/lib/models/flowModel.js +1133 -0
  175. package/lib/models/forkFlowModel.d.ts +83 -0
  176. package/lib/models/forkFlowModel.js +257 -0
  177. package/lib/models/index.d.ts +14 -0
  178. package/lib/models/index.js +40 -0
  179. package/lib/provider.d.ts +22 -0
  180. package/lib/provider.js +114 -0
  181. package/lib/resources/apiResource.d.ts +34 -0
  182. package/lib/resources/apiResource.js +153 -0
  183. package/lib/resources/baseRecordResource.d.ts +61 -0
  184. package/lib/resources/baseRecordResource.js +264 -0
  185. package/lib/resources/filterItem.d.ts +33 -0
  186. package/lib/resources/filterItem.js +93 -0
  187. package/lib/resources/flowResource.d.ts +45 -0
  188. package/lib/resources/flowResource.js +146 -0
  189. package/lib/resources/index.d.ts +15 -0
  190. package/lib/resources/index.js +42 -0
  191. package/lib/resources/multiRecordResource.d.ts +53 -0
  192. package/lib/resources/multiRecordResource.js +230 -0
  193. package/lib/resources/singleRecordResource.d.ts +23 -0
  194. package/lib/resources/singleRecordResource.js +111 -0
  195. package/lib/resources/sqlResource.d.ts +73 -0
  196. package/lib/resources/sqlResource.js +294 -0
  197. package/lib/runjs-context/contexts/FlowRunJSContext.d.ts +38 -0
  198. package/lib/runjs-context/contexts/FlowRunJSContext.js +217 -0
  199. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.d.ts +16 -0
  200. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +66 -0
  201. package/lib/runjs-context/contexts/JSBlockRunJSContext.d.ts +16 -0
  202. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +78 -0
  203. package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.d.ts +12 -0
  204. package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.js +59 -0
  205. package/lib/runjs-context/contexts/JSFieldRunJSContext.d.ts +16 -0
  206. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +70 -0
  207. package/lib/runjs-context/contexts/JSItemRunJSContext.d.ts +16 -0
  208. package/lib/runjs-context/contexts/JSItemRunJSContext.js +66 -0
  209. package/lib/runjs-context/contexts/JSRecordActionRunJSContext.d.ts +12 -0
  210. package/lib/runjs-context/contexts/JSRecordActionRunJSContext.js +61 -0
  211. package/lib/runjs-context/contexts/LinkageRunJSContext.d.ts +12 -0
  212. package/lib/runjs-context/contexts/LinkageRunJSContext.js +62 -0
  213. package/lib/runjs-context/helpers.d.ts +17 -0
  214. package/lib/runjs-context/helpers.js +79 -0
  215. package/lib/runjs-context/index.d.ts +19 -0
  216. package/lib/runjs-context/index.js +57 -0
  217. package/lib/runjs-context/registry.d.ts +17 -0
  218. package/lib/runjs-context/registry.js +93 -0
  219. package/lib/runjs-context/snippets/global/api-request-get.snippet.d.ts +16 -0
  220. package/lib/runjs-context/snippets/global/api-request-get.snippet.js +42 -0
  221. package/lib/runjs-context/snippets/global/api-request-post.snippet.d.ts +16 -0
  222. package/lib/runjs-context/snippets/global/api-request-post.snippet.js +42 -0
  223. package/lib/runjs-context/snippets/global/console-log-ctx.snippet.d.ts +16 -0
  224. package/lib/runjs-context/snippets/global/console-log-ctx.snippet.js +41 -0
  225. package/lib/runjs-context/snippets/global/copy-record-json.snippet.d.ts +11 -0
  226. package/lib/runjs-context/snippets/global/copy-record-json.snippet.js +42 -0
  227. package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.d.ts +11 -0
  228. package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.js +42 -0
  229. package/lib/runjs-context/snippets/global/log-json-record.snippet.d.ts +11 -0
  230. package/lib/runjs-context/snippets/global/log-json-record.snippet.js +42 -0
  231. package/lib/runjs-context/snippets/global/message-error.snippet.d.ts +11 -0
  232. package/lib/runjs-context/snippets/global/message-error.snippet.js +41 -0
  233. package/lib/runjs-context/snippets/global/message-success.snippet.d.ts +11 -0
  234. package/lib/runjs-context/snippets/global/message-success.snippet.js +41 -0
  235. package/lib/runjs-context/snippets/global/notification-open.snippet.d.ts +16 -0
  236. package/lib/runjs-context/snippets/global/notification-open.snippet.js +43 -0
  237. package/lib/runjs-context/snippets/global/open-view-dialog.snippet.d.ts +11 -0
  238. package/lib/runjs-context/snippets/global/open-view-dialog.snippet.js +47 -0
  239. package/lib/runjs-context/snippets/global/open-view-drawer.snippet.d.ts +11 -0
  240. package/lib/runjs-context/snippets/global/open-view-drawer.snippet.js +47 -0
  241. package/lib/runjs-context/snippets/global/requireAsync.snippet.d.ts +16 -0
  242. package/lib/runjs-context/snippets/global/requireAsync.snippet.js +46 -0
  243. package/lib/runjs-context/snippets/global/sleep.snippet.d.ts +16 -0
  244. package/lib/runjs-context/snippets/global/sleep.snippet.js +43 -0
  245. package/lib/runjs-context/snippets/global/try-catch-async.snippet.d.ts +16 -0
  246. package/lib/runjs-context/snippets/global/try-catch-async.snippet.js +44 -0
  247. package/lib/runjs-context/snippets/global/view-navigation-push.snippet.d.ts +11 -0
  248. package/lib/runjs-context/snippets/global/view-navigation-push.snippet.js +44 -0
  249. package/lib/runjs-context/snippets/global/window-open.snippet.d.ts +16 -0
  250. package/lib/runjs-context/snippets/global/window-open.snippet.js +41 -0
  251. package/lib/runjs-context/snippets/index.d.ts +11 -0
  252. package/lib/runjs-context/snippets/index.js +94 -0
  253. package/lib/runjs-context/snippets/libs/echarts-init.snippet.d.ts +15 -0
  254. package/lib/runjs-context/snippets/libs/echarts-init.snippet.js +46 -0
  255. package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.d.ts +15 -0
  256. package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.js +44 -0
  257. package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.d.ts +15 -0
  258. package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.js +43 -0
  259. package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.d.ts +15 -0
  260. package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.js +43 -0
  261. package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.d.ts +15 -0
  262. package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.js +40 -0
  263. package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.d.ts +15 -0
  264. package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.js +46 -0
  265. package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.d.ts +15 -0
  266. package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.js +42 -0
  267. package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.d.ts +15 -0
  268. package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.js +46 -0
  269. package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.d.ts +15 -0
  270. package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.js +41 -0
  271. package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.d.ts +15 -0
  272. package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.js +41 -0
  273. package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.d.ts +15 -0
  274. package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.js +46 -0
  275. package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.d.ts +11 -0
  276. package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.js +45 -0
  277. package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.d.ts +15 -0
  278. package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.js +56 -0
  279. package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.d.ts +15 -0
  280. package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.js +42 -0
  281. package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.d.ts +15 -0
  282. package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.js +41 -0
  283. package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.d.ts +15 -0
  284. package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.js +40 -0
  285. package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.d.ts +15 -0
  286. package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.js +44 -0
  287. package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.d.ts +15 -0
  288. package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.js +60 -0
  289. package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.d.ts +15 -0
  290. package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.js +59 -0
  291. package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.d.ts +15 -0
  292. package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.js +60 -0
  293. package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.d.ts +15 -0
  294. package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.js +60 -0
  295. package/lib/runjs-context/snippets/types.d.ts +16 -0
  296. package/lib/runjs-context/snippets/types.js +24 -0
  297. package/lib/types.d.ts +398 -0
  298. package/lib/types.js +43 -0
  299. package/lib/utils/autoFlowError.d.ts +16 -0
  300. package/lib/utils/autoFlowError.js +53 -0
  301. package/lib/utils/constants.d.ts +29 -0
  302. package/lib/utils/constants.js +77 -0
  303. package/lib/utils/context.d.ts +40 -0
  304. package/lib/utils/context.js +63 -0
  305. package/lib/utils/createCollectionContextMeta.d.ts +11 -0
  306. package/lib/utils/createCollectionContextMeta.js +117 -0
  307. package/lib/utils/exceptions.d.ts +22 -0
  308. package/lib/utils/exceptions.js +62 -0
  309. package/lib/utils/flow-definitions.d.ts +11 -0
  310. package/lib/utils/flow-definitions.js +40 -0
  311. package/lib/utils/index.d.ts +22 -0
  312. package/lib/utils/index.js +117 -0
  313. package/lib/utils/inheritance.d.ts +16 -0
  314. package/lib/utils/inheritance.js +53 -0
  315. package/lib/utils/params-resolvers.d.ts +51 -0
  316. package/lib/utils/params-resolvers.js +309 -0
  317. package/lib/utils/parsePathnameToViewParams.d.ts +34 -0
  318. package/lib/utils/parsePathnameToViewParams.js +84 -0
  319. package/lib/utils/safeGlobals.d.ts +16 -0
  320. package/lib/utils/safeGlobals.js +179 -0
  321. package/lib/utils/schema-utils.d.ts +40 -0
  322. package/lib/utils/schema-utils.js +161 -0
  323. package/lib/utils/serverContextParams.d.ts +28 -0
  324. package/lib/utils/serverContextParams.js +106 -0
  325. package/lib/utils/setupRuntimeContextSteps.d.ts +19 -0
  326. package/lib/utils/setupRuntimeContextSteps.js +88 -0
  327. package/lib/utils/translation.d.ts +18 -0
  328. package/lib/utils/translation.js +58 -0
  329. package/lib/utils/variablesParams.d.ts +51 -0
  330. package/lib/utils/variablesParams.js +150 -0
  331. package/lib/views/DialogComponent.d.ts +22 -0
  332. package/lib/views/DialogComponent.js +98 -0
  333. package/lib/views/DrawerComponent.d.ts +11 -0
  334. package/lib/views/DrawerComponent.js +101 -0
  335. package/lib/views/FlowView.d.ts +76 -0
  336. package/lib/views/FlowView.js +81 -0
  337. package/lib/views/PageComponent.d.ts +10 -0
  338. package/lib/views/PageComponent.js +167 -0
  339. package/lib/views/ViewNavigation.d.ts +45 -0
  340. package/lib/views/ViewNavigation.js +97 -0
  341. package/lib/views/createViewMeta.d.ts +16 -0
  342. package/lib/views/createViewMeta.js +171 -0
  343. package/lib/views/index.d.ts +13 -0
  344. package/lib/views/index.js +48 -0
  345. package/lib/views/useDialog.d.ts +32 -0
  346. package/lib/views/useDialog.js +199 -0
  347. package/lib/views/useDrawer.d.ts +33 -0
  348. package/lib/views/useDrawer.js +206 -0
  349. package/lib/views/usePage.d.ts +32 -0
  350. package/lib/views/usePage.js +193 -0
  351. package/lib/views/usePatchElement.d.ts +10 -0
  352. package/lib/views/usePatchElement.js +54 -0
  353. package/lib/views/usePopover.d.ts +17 -0
  354. package/lib/views/usePopover.js +159 -0
  355. package/package.json +37 -0
  356. package/src/ContextPathProxy.ts +45 -0
  357. package/src/ElementProxy.ts +69 -0
  358. package/src/FlowContextProvider.tsx +40 -0
  359. package/src/FlowDefinition.ts +275 -0
  360. package/src/JSRunner.ts +84 -0
  361. package/src/ReactView.tsx +104 -0
  362. package/src/ViewScopedFlowEngine.ts +75 -0
  363. package/src/__tests__/ElementProxy.test.ts +51 -0
  364. package/src/__tests__/JSRunner.test.ts +92 -0
  365. package/src/__tests__/ReactView.test.tsx +63 -0
  366. package/src/__tests__/context-path-proxy.test.ts +35 -0
  367. package/src/__tests__/flow-engine.test.ts +189 -0
  368. package/src/__tests__/flowContext.test.ts +2012 -0
  369. package/src/__tests__/flowEngine.saveModel.test.ts +171 -0
  370. package/src/__tests__/flowI18n.test.ts +28 -0
  371. package/src/__tests__/flowModel.getFlows.test.ts +61 -0
  372. package/src/__tests__/flowModel.openView.navigation.test.ts +78 -0
  373. package/src/__tests__/flowRuntimeContext.test.ts +187 -0
  374. package/src/__tests__/flowSettings.open.test.tsx +1920 -0
  375. package/src/__tests__/flowSettings.test.ts +566 -0
  376. package/src/__tests__/globalFlowRegistry.test.ts +77 -0
  377. package/src/__tests__/isFieldInterfaceMatch.test.ts +51 -0
  378. package/src/__tests__/metaTreeNodeCache.test.ts +234 -0
  379. package/src/__tests__/path-aggregation.test.ts +85 -0
  380. package/src/__tests__/provider.test.tsx +28 -0
  381. package/src/__tests__/renderHiddenInConfig.test.tsx +91 -0
  382. package/src/__tests__/runjsContext.test.ts +60 -0
  383. package/src/__tests__/viewScopedFlowEngine.test.ts +212 -0
  384. package/src/acl/Acl.tsx +109 -0
  385. package/src/acl/__tests__/Acl.test.tsx +72 -0
  386. package/src/action-registry/BaseActionRegistry.ts +46 -0
  387. package/src/action-registry/EngineActionRegistry.ts +32 -0
  388. package/src/action-registry/ModelActionRegistry.ts +75 -0
  389. package/src/action-registry/__tests__/engineActionRegistry.test.ts +43 -0
  390. package/src/action-registry/__tests__/modelActionRegistry.test.ts +107 -0
  391. package/src/components/DynamicFlowsEditor.tsx +318 -0
  392. package/src/components/FieldModelRenderer.tsx +62 -0
  393. package/src/components/FlowContextSelector.tsx +255 -0
  394. package/src/components/FlowErrorFallback.tsx +316 -0
  395. package/src/components/FlowModelRenderer.tsx +428 -0
  396. package/src/components/FormItem.tsx +130 -0
  397. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +226 -0
  398. package/src/components/common/FlowSettingsButton.tsx +30 -0
  399. package/src/components/common/index.ts +10 -0
  400. package/src/components/common/withFlowDesignMode.tsx +49 -0
  401. package/src/components/dnd/getMousePositionOnElement.ts +115 -0
  402. package/src/components/dnd/index.tsx +128 -0
  403. package/src/components/dnd/moveBlock.ts +379 -0
  404. package/src/components/index.ts +20 -0
  405. package/src/components/settings/independents/dropdown/FlowsDropdownButton.tsx +279 -0
  406. package/src/components/settings/independents/dropdown/index.ts +10 -0
  407. package/src/components/settings/independents/index.ts +2 -0
  408. package/src/components/settings/index.ts +11 -0
  409. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +617 -0
  410. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +292 -0
  411. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +655 -0
  412. package/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx +446 -0
  413. package/src/components/settings/wrappers/contextual/StepSettings.tsx +109 -0
  414. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +217 -0
  415. package/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx +32 -0
  416. package/src/components/settings/wrappers/contextual/index.ts +15 -0
  417. package/src/components/settings/wrappers/embedded/FlowSettings.tsx +258 -0
  418. package/src/components/settings/wrappers/embedded/FlowsSettings.tsx +111 -0
  419. package/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx +96 -0
  420. package/src/components/settings/wrappers/embedded/index.ts +11 -0
  421. package/src/components/settings/wrappers/index.ts +5 -0
  422. package/src/components/subModel/AddSubModelButton.tsx +575 -0
  423. package/src/components/subModel/LazyDropdown.tsx +714 -0
  424. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +1185 -0
  425. package/src/components/subModel/__tests__/buildWrapperFieldChildren.test.ts +192 -0
  426. package/src/components/subModel/__tests__/utils.test.ts +425 -0
  427. package/src/components/subModel/index.ts +12 -0
  428. package/src/components/subModel/utils.ts +278 -0
  429. package/src/components/variables/InlineVariableTag.tsx +97 -0
  430. package/src/components/variables/SlateVariableEditor.tsx +384 -0
  431. package/src/components/variables/VariableInput.tsx +342 -0
  432. package/src/components/variables/VariableTag.tsx +123 -0
  433. package/src/components/variables/VariableTrigger.tsx +116 -0
  434. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +553 -0
  435. package/src/components/variables/__tests__/VariableInput.test.tsx +550 -0
  436. package/src/components/variables/__tests__/VariableTag.test.tsx +347 -0
  437. package/src/components/variables/__tests__/test-utils.tsx +62 -0
  438. package/src/components/variables/__tests__/utils.test.ts +310 -0
  439. package/src/components/variables/index.ts +16 -0
  440. package/src/components/variables/types.ts +100 -0
  441. package/src/components/variables/useResolvedMetaTree.ts +76 -0
  442. package/src/components/variables/utils.ts +192 -0
  443. package/src/data-source/__tests__/collection.test.ts +58 -0
  444. package/src/data-source/__tests__/index.test.ts +82 -0
  445. package/src/data-source/__tests__/jioToJoiSchema.test.ts +56 -0
  446. package/src/data-source/index.ts +812 -0
  447. package/src/data-source/jioToJoiSchema.ts +103 -0
  448. package/src/decorators/index.ts +10 -0
  449. package/src/decorators/largeField.ts +14 -0
  450. package/src/emitter.ts +33 -0
  451. package/src/event-registry/BaseEventRegistry.ts +40 -0
  452. package/src/event-registry/EngineEventRegistry.ts +26 -0
  453. package/src/event-registry/ModelEventRegistry.ts +69 -0
  454. package/src/event-registry/__tests__/engineEventRegistry.test.ts +48 -0
  455. package/src/executor/FlowExecutor.ts +256 -0
  456. package/src/executor/__tests__/eventStep.test.ts +157 -0
  457. package/src/executor/__tests__/flowExecutor.test.ts +163 -0
  458. package/src/flow-registry/BaseFlowRegistry.ts +91 -0
  459. package/src/flow-registry/GlobalFlowRegistry.ts +82 -0
  460. package/src/flow-registry/InstanceFlowRegistry.ts +39 -0
  461. package/src/flow-registry/__tests__/globalFlowRegistry.test.ts +141 -0
  462. package/src/flow-registry/__tests__/instance-and-global-registry.test.ts +67 -0
  463. package/src/flow-registry/__tests__/instanceFlowRegistry.test.ts +83 -0
  464. package/src/flow-registry/index.ts +12 -0
  465. package/src/flowContext.ts +1639 -0
  466. package/src/flowEngine.ts +905 -0
  467. package/src/flowI18n.ts +96 -0
  468. package/src/flowSettings.ts +1045 -0
  469. package/src/hooks/index.ts +15 -0
  470. package/src/hooks/useApplyAutoFlows.ts +51 -0
  471. package/src/hooks/useFlowModel.tsx +59 -0
  472. package/src/hooks/useFlowModelById.ts +37 -0
  473. package/src/hooks/useFlowSettingsContext.tsx +37 -0
  474. package/src/hooks/useFlowStep.tsx +19 -0
  475. package/src/hooks/useNiceDropdownMaxHeight.ts +34 -0
  476. package/src/index.ts +38 -0
  477. package/src/locale/en-US.json +61 -0
  478. package/src/locale/index.ts +38 -0
  479. package/src/locale/zh-CN.json +61 -0
  480. package/src/models/CollectionFieldModel.tsx +269 -0
  481. package/src/models/DisplayItemModel.tsx +13 -0
  482. package/src/models/EditableItemModel.tsx +13 -0
  483. package/src/models/FilterableItemModel.tsx +13 -0
  484. package/src/models/__tests__/CollectionFieldModel.test.ts +122 -0
  485. package/src/models/__tests__/defaultParams-on-create.test.ts +83 -0
  486. package/src/models/__tests__/flow-model-oninit.test.ts +44 -0
  487. package/src/models/__tests__/flowModel.actions.integration.test.ts +100 -0
  488. package/src/models/__tests__/flowModel.getFlows.sort.test.ts +100 -0
  489. package/src/models/__tests__/flowModel.test.ts +2746 -0
  490. package/src/models/__tests__/flowRegistry.test.ts +512 -0
  491. package/src/models/__tests__/forkFlowModel.test.ts +1047 -0
  492. package/src/models/__tests__/model-actions.test.ts +70 -0
  493. package/src/models/__tests__/model-events.test.ts +69 -0
  494. package/src/models/flowModel.tsx +1398 -0
  495. package/src/models/forkFlowModel.ts +287 -0
  496. package/src/models/index.ts +17 -0
  497. package/src/provider.tsx +101 -0
  498. package/src/resources/__tests__/apiResource.test.ts +201 -0
  499. package/src/resources/__tests__/baseRecordResource.test.ts +262 -0
  500. package/src/resources/__tests__/filterItem.test.ts +260 -0
  501. package/src/resources/__tests__/flowResource.test.ts +127 -0
  502. package/src/resources/apiResource.ts +148 -0
  503. package/src/resources/baseRecordResource.ts +279 -0
  504. package/src/resources/filterItem.ts +74 -0
  505. package/src/resources/flowResource.ts +143 -0
  506. package/src/resources/index.ts +17 -0
  507. package/src/resources/multiRecordResource.ts +219 -0
  508. package/src/resources/singleRecordResource.ts +83 -0
  509. package/src/resources/sqlResource.ts +299 -0
  510. package/src/runjs-context/contexts/FlowRunJSContext.ts +190 -0
  511. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +39 -0
  512. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +52 -0
  513. package/src/runjs-context/contexts/JSCollectionActionRunJSContext.ts +32 -0
  514. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +43 -0
  515. package/src/runjs-context/contexts/JSItemRunJSContext.ts +39 -0
  516. package/src/runjs-context/contexts/JSRecordActionRunJSContext.ts +34 -0
  517. package/src/runjs-context/contexts/LinkageRunJSContext.ts +35 -0
  518. package/src/runjs-context/helpers.ts +56 -0
  519. package/src/runjs-context/index.ts +20 -0
  520. package/src/runjs-context/registry.ts +65 -0
  521. package/src/runjs-context/snippets/global/api-request-get.snippet.ts +20 -0
  522. package/src/runjs-context/snippets/global/api-request-post.snippet.ts +20 -0
  523. package/src/runjs-context/snippets/global/console-log-ctx.snippet.ts +19 -0
  524. package/src/runjs-context/snippets/global/copy-record-json.snippet.ts +21 -0
  525. package/src/runjs-context/snippets/global/copy-to-clipboard.snippet.ts +21 -0
  526. package/src/runjs-context/snippets/global/log-json-record.snippet.ts +21 -0
  527. package/src/runjs-context/snippets/global/message-error.snippet.ts +20 -0
  528. package/src/runjs-context/snippets/global/message-success.snippet.ts +20 -0
  529. package/src/runjs-context/snippets/global/notification-open.snippet.ts +21 -0
  530. package/src/runjs-context/snippets/global/open-view-dialog.snippet.ts +26 -0
  531. package/src/runjs-context/snippets/global/open-view-drawer.snippet.ts +26 -0
  532. package/src/runjs-context/snippets/global/requireAsync.snippet.ts +24 -0
  533. package/src/runjs-context/snippets/global/sleep.snippet.ts +21 -0
  534. package/src/runjs-context/snippets/global/try-catch-async.snippet.ts +22 -0
  535. package/src/runjs-context/snippets/global/view-navigation-push.snippet.ts +23 -0
  536. package/src/runjs-context/snippets/global/window-open.snippet.ts +19 -0
  537. package/src/runjs-context/snippets/index.ts +59 -0
  538. package/src/runjs-context/snippets/libs/echarts-init.snippet.ts +24 -0
  539. package/src/runjs-context/snippets/scene/actions/collection-selected-count.snippet.ts +22 -0
  540. package/src/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.ts +21 -0
  541. package/src/runjs-context/snippets/scene/actions/record-id-message.snippet.ts +21 -0
  542. package/src/runjs-context/snippets/scene/actions/run-action-basic.snippet.ts +18 -0
  543. package/src/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.ts +29 -0
  544. package/src/runjs-context/snippets/scene/jsblock/append-style.snippet.ts +20 -0
  545. package/src/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.ts +24 -0
  546. package/src/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.ts +19 -0
  547. package/src/runjs-context/snippets/scene/jsblock/render-basic.snippet.ts +24 -0
  548. package/src/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.ts +24 -0
  549. package/src/runjs-context/snippets/scene/jsblock/render-card.snippet.ts +30 -0
  550. package/src/runjs-context/snippets/scene/jsblock/render-react.snippet.ts +34 -0
  551. package/src/runjs-context/snippets/scene/jsfield/color-by-value.snippet.ts +20 -0
  552. package/src/runjs-context/snippets/scene/jsfield/format-number.snippet.ts +19 -0
  553. package/src/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.ts +18 -0
  554. package/src/runjs-context/snippets/scene/jsitem/render-basic.snippet.ts +27 -0
  555. package/src/runjs-context/snippets/scene/linkage/set-disabled.snippet.ts +38 -0
  556. package/src/runjs-context/snippets/scene/linkage/set-field-value.snippet.ts +37 -0
  557. package/src/runjs-context/snippets/scene/linkage/set-required.snippet.ts +38 -0
  558. package/src/runjs-context/snippets/scene/linkage/toggle-visible.snippet.ts +38 -0
  559. package/src/runjs-context/snippets/types.ts +17 -0
  560. package/src/types.ts +474 -0
  561. package/src/utils/__tests__/context.test.ts +93 -0
  562. package/src/utils/__tests__/params-resolvers.test.ts +652 -0
  563. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +104 -0
  564. package/src/utils/__tests__/safeGlobals.test.ts +29 -0
  565. package/src/utils/__tests__/utils.test.ts +1021 -0
  566. package/src/utils/__tests__/variablesParams.test.ts +52 -0
  567. package/src/utils/autoFlowError.ts +29 -0
  568. package/src/utils/constants.ts +60 -0
  569. package/src/utils/context.ts +70 -0
  570. package/src/utils/createCollectionContextMeta.ts +122 -0
  571. package/src/utils/exceptions.ts +36 -0
  572. package/src/utils/flow-definitions.ts +16 -0
  573. package/src/utils/index.ts +63 -0
  574. package/src/utils/inheritance.ts +39 -0
  575. package/src/utils/params-resolvers.ts +482 -0
  576. package/src/utils/parsePathnameToViewParams.ts +103 -0
  577. package/src/utils/safeGlobals.ts +188 -0
  578. package/src/utils/schema-utils.ts +201 -0
  579. package/src/utils/serverContextParams.ts +111 -0
  580. package/src/utils/setupRuntimeContextSteps.ts +89 -0
  581. package/src/utils/translation.ts +37 -0
  582. package/src/utils/variablesParams.ts +175 -0
  583. package/src/views/DialogComponent.tsx +79 -0
  584. package/src/views/DrawerComponent.tsx +72 -0
  585. package/src/views/FlowView.tsx +103 -0
  586. package/src/views/PageComponent.tsx +150 -0
  587. package/src/views/ViewNavigation.ts +122 -0
  588. package/src/views/__tests__/FlowView.test.ts +31 -0
  589. package/src/views/__tests__/ViewNavigation.test.ts +191 -0
  590. package/src/views/__tests__/usePatchElement.test.tsx +28 -0
  591. package/src/views/createViewMeta.ts +157 -0
  592. package/src/views/index.tsx +14 -0
  593. package/src/views/useDialog.tsx +192 -0
  594. package/src/views/useDrawer.tsx +205 -0
  595. package/src/views/usePage.tsx +182 -0
  596. package/src/views/usePatchElement.tsx +27 -0
  597. package/src/views/usePopover.tsx +131 -0
@@ -0,0 +1,1920 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, it, expect, vi, afterEach } from 'vitest';
11
+ import { screen } from '@testing-library/react';
12
+ import { FlowSettings } from '../flowSettings';
13
+ import { FlowModel } from '../models';
14
+ import { FlowEngine } from '../flowEngine';
15
+
16
+ // We will stub viewer directly on model.context in tests
17
+
18
+ // Lightweight mocks for Formily/Antd components used during compile
19
+ vi.mock('@formily/antd-v5', () => ({
20
+ Form: () => 'Form',
21
+ FormDialog: () => 'FormDialog',
22
+ FormDrawer: () => 'FormDrawer',
23
+ FormItem: () => 'FormItem',
24
+ FormLayout: () => 'FormLayout',
25
+ FormGrid: () => 'FormGrid',
26
+ FormStep: () => 'FormStep',
27
+ FormTab: () => 'FormTab',
28
+ FormCollapse: () => 'FormCollapse',
29
+ FormButtonGroup: () => 'FormButtonGroup',
30
+ Input: () => 'Input',
31
+ NumberPicker: () => 'NumberPicker',
32
+ Password: () => 'Password',
33
+ Select: () => 'Select',
34
+ SelectTable: () => 'SelectTable',
35
+ Cascader: () => 'Cascader',
36
+ TreeSelect: () => 'TreeSelect',
37
+ Transfer: () => 'Transfer',
38
+ DatePicker: () => 'DatePicker',
39
+ TimePicker: () => 'TimePicker',
40
+ Checkbox: () => 'Checkbox',
41
+ Radio: () => 'Radio',
42
+ Switch: () => 'Switch',
43
+ ArrayBase: () => 'ArrayBase',
44
+ ArrayCards: () => 'ArrayCards',
45
+ ArrayCollapse: () => 'ArrayCollapse',
46
+ ArrayItems: () => 'ArrayItems',
47
+ ArrayTable: () => 'ArrayTable',
48
+ ArrayTabs: () => 'ArrayTabs',
49
+ Upload: () => 'Upload',
50
+ Space: () => 'Space',
51
+ Editable: () => 'Editable',
52
+ PreviewText: () => 'PreviewText',
53
+ Submit: () => 'Submit',
54
+ Reset: () => 'Reset',
55
+ }));
56
+ vi.mock('antd', () => {
57
+ const Collapse = Object.assign((props: any) => 'Collapse', { Panel: (props: any) => 'Panel' });
58
+ const FormItem = ({ children }: any) => children ?? 'FormItem';
59
+ const Form = Object.assign((props: any) => 'Form', {
60
+ Item: FormItem,
61
+ useForm: () => [{ setFieldsValue: (_: any) => {} }],
62
+ });
63
+ const Input: any = (props: any) => 'Input';
64
+ Input.TextArea = (props: any) => 'TextArea';
65
+ const InputNumber = (props: any) => 'InputNumber';
66
+ const Select = (props: any) => 'Select';
67
+ const Switch = (props: any) => 'Switch';
68
+ const Alert = (props: any) => 'Alert';
69
+ return {
70
+ Button: () => 'Button',
71
+ Space: () => 'Space',
72
+ Tabs: () => 'Tabs',
73
+ Collapse,
74
+ Result: () => 'Result',
75
+ Form,
76
+ Input,
77
+ InputNumber,
78
+ Select,
79
+ Switch,
80
+ Alert,
81
+ Typography: {
82
+ Paragraph: ({ children }: any) => children ?? 'Paragraph',
83
+ Text: ({ children }: any) => children ?? 'Text',
84
+ },
85
+ };
86
+ });
87
+
88
+ // helper to locate the primary Button element in a React element tree
89
+ // Updated to work with new Cancel/Save component structure
90
+ const findPrimaryButton = async (node: any) => {
91
+ const { Button } = await import('antd');
92
+ const walk = (n: any): any => {
93
+ if (!n || typeof n !== 'object') return null;
94
+
95
+ // Look for Button with type='primary'
96
+ if (n.type === Button && n?.props?.type === 'primary') return n;
97
+
98
+ // Also look for the Save component (function component that creates primary button)
99
+ if (typeof n.type === 'function' && n.type.name === 'Save') {
100
+ // Return the actual Button element created by Save component
101
+ const buttonEl = n.type(n.props);
102
+ if (buttonEl && buttonEl.type === Button && buttonEl.props?.type === 'primary') {
103
+ return { ...buttonEl, props: { ...buttonEl.props, onClick: n.props?.onClick || buttonEl.props?.onClick } };
104
+ }
105
+ }
106
+
107
+ const children = n.props?.children;
108
+ if (!children) return null;
109
+ if (Array.isArray(children)) {
110
+ for (const c of children) {
111
+ const r = walk(c);
112
+ if (r) return r;
113
+ }
114
+ return null;
115
+ }
116
+ return walk(children);
117
+ };
118
+ return walk(node);
119
+ };
120
+
121
+ // helper to locate the cancel Button (the non-primary one with text 'Cancel')
122
+ // Updated to work with new Cancel/Save component structure
123
+ const findCancelButton = async (node: any) => {
124
+ const { Button } = await import('antd');
125
+ const walk = (n: any): any => {
126
+ if (!n || typeof n !== 'object') return null;
127
+
128
+ // Look for Button without type and with 'Cancel' text
129
+ const isBtn = n.type === Button;
130
+ const isCancel = isBtn && !n?.props?.type && n?.props?.children === 'Cancel';
131
+ if (isCancel) return n;
132
+
133
+ // Also look for the Cancel component (function component that creates cancel button)
134
+ if (typeof n.type === 'function' && n.type.name === 'Cancel') {
135
+ // Return the actual Button element created by Cancel component
136
+ const buttonEl = n.type(n.props);
137
+ if (buttonEl && buttonEl.type === Button && !buttonEl.props?.type) {
138
+ return { ...buttonEl, props: { ...buttonEl.props, onClick: n.props?.onClick || buttonEl.props?.onClick } };
139
+ }
140
+ }
141
+
142
+ const children = n.props?.children;
143
+ if (!children) return null;
144
+ if (Array.isArray(children)) {
145
+ for (const c of children) {
146
+ const r = walk(c);
147
+ if (r) return r;
148
+ }
149
+ return null;
150
+ }
151
+ return walk(children);
152
+ };
153
+ return walk(node);
154
+ };
155
+
156
+ describe('FlowSettings.open rendering behavior', () => {
157
+ afterEach(() => {
158
+ document.querySelectorAll('[data-testid]')?.forEach((n) => n.remove());
159
+ vi.clearAllMocks();
160
+ });
161
+
162
+ // 创建测试特定的 FlowModel 子类,确保每个测试有独立的 globalFlowRegistry
163
+ const createIsolatedFlowModel = (testId: string) => {
164
+ // 动态创建匿名类,每个类都有独立的 globalFlowRegistry
165
+ return class extends FlowModel {
166
+ static testId = testId; // 用于调试识别
167
+ };
168
+ };
169
+
170
+ it('renders single-step form directly when flowKey+stepKey provided (no Collapse)', async () => {
171
+ const engine = new FlowEngine();
172
+ const flowSettings = new FlowSettings(engine);
173
+ const TestFlowModel = createIsolatedFlowModel('test-1');
174
+ const model = new TestFlowModel({ uid: 'm-open-1', flowEngine: engine });
175
+
176
+ // Register a dummy flow with one step
177
+ TestFlowModel.registerFlow({
178
+ key: 'testFlow',
179
+ steps: {
180
+ general: {
181
+ title: 'General',
182
+ uiSchema: {
183
+ field1: { type: 'string', 'x-component': 'Input' },
184
+ },
185
+ },
186
+ },
187
+ });
188
+
189
+ // stub viewer
190
+ model.context.defineProperty('viewer', {
191
+ value: {
192
+ dialog: ({ content }) => {
193
+ const dialog = {
194
+ close: () => container.remove(),
195
+ Footer: () => null,
196
+ } as any;
197
+ const container = document.createElement('div');
198
+ container.setAttribute('data-testid', 'flow-settings-container');
199
+ // execute content function to ensure render path doesn't throw
200
+ if (typeof content === 'function') {
201
+ content(dialog, { defineMethod: vi.fn() });
202
+ }
203
+ document.body.appendChild(container);
204
+ return dialog;
205
+ },
206
+ drawer: ({ content }) => {
207
+ const dialog = {
208
+ close: () => container.remove?.(),
209
+ Footer: () => null,
210
+ } as any;
211
+ const container = document.createElement('div');
212
+ container.setAttribute('data-testid', 'flow-settings-container');
213
+ if (typeof content === 'function') {
214
+ content(dialog, { defineMethod: vi.fn() });
215
+ }
216
+ document.body.appendChild(container);
217
+ return dialog;
218
+ },
219
+ },
220
+ });
221
+
222
+ await flowSettings.open({ model, flowKey: 'testFlow', stepKey: 'general', uiMode: 'dialog' } as any);
223
+
224
+ const container = screen.getByTestId('flow-settings-container');
225
+ expect(container).toBeInTheDocument();
226
+ // Should NOT render Collapse markers since we render a plain form
227
+ expect(container.querySelector('.ant-collapse')).toBeNull();
228
+ });
229
+
230
+ it('renders Collapse when only one step matched but no stepKey provided', async () => {
231
+ const engine = new FlowEngine();
232
+ const flowSettings = new FlowSettings(engine);
233
+ const TestFlowModel = createIsolatedFlowModel('test-2');
234
+ const model = new TestFlowModel({ uid: 'm-open-2', flowEngine: engine });
235
+
236
+ TestFlowModel.registerFlow({
237
+ key: 'testFlow2',
238
+ steps: {
239
+ general: {
240
+ title: 'General',
241
+ uiSchema: {
242
+ field1: { type: 'string', 'x-component': 'Input' },
243
+ },
244
+ },
245
+ },
246
+ });
247
+
248
+ // stub viewer
249
+ model.context.defineProperty('viewer', {
250
+ value: {
251
+ dialog: ({ content }) => {
252
+ const dialog = {
253
+ close: () => container.remove(),
254
+ Footer: () => null,
255
+ } as any;
256
+ const container = document.createElement('div');
257
+ container.setAttribute('data-testid', 'flow-settings-container');
258
+ if (typeof content === 'function') {
259
+ content(dialog, { defineMethod: vi.fn() });
260
+ }
261
+ document.body.appendChild(container);
262
+ return dialog;
263
+ },
264
+ drawer: ({ content }) => {
265
+ const dialog = {
266
+ close: () => container.remove?.(),
267
+ Footer: () => null,
268
+ } as any;
269
+ const container = document.createElement('div');
270
+ container.setAttribute('data-testid', 'flow-settings-container');
271
+ if (typeof content === 'function') {
272
+ content(dialog, { defineMethod: vi.fn() });
273
+ }
274
+ document.body.appendChild(container);
275
+ return dialog;
276
+ },
277
+ },
278
+ });
279
+
280
+ await flowSettings.open({ model, flowKey: 'testFlow2', uiMode: 'dialog' } as any);
281
+
282
+ const container = screen.getByTestId('flow-settings-container');
283
+ expect(container).toBeInTheDocument();
284
+ // In our minimal mock we don't render real Collapse DOM, but we can check that the FlowViewer was invoked.
285
+ // For robustness, just assert container exists (behavior difference is ensured by code path and not by DOM markers here).
286
+ expect(container).toBeTruthy();
287
+ });
288
+
289
+ it('shows info when there are no configurable steps (entries length 0)', async () => {
290
+ const engine = new FlowEngine();
291
+ const flowSettings = new FlowSettings(engine);
292
+ const TestFlowModel = createIsolatedFlowModel('test-3');
293
+ const model = new TestFlowModel({ uid: 'm-open-none', flowEngine: engine });
294
+
295
+ // message spy
296
+ const info = vi.fn();
297
+ const error = vi.fn();
298
+ const success = vi.fn();
299
+ model.context.defineProperty('message', { value: { info, error, success } });
300
+
301
+ // viewer spies (should NOT be called)
302
+ const dialog = vi.fn();
303
+ const drawer = vi.fn();
304
+ model.context.defineProperty('viewer', { value: { dialog, drawer } });
305
+
306
+ await flowSettings.open({ model } as any);
307
+
308
+ expect(info).toHaveBeenCalled();
309
+ expect(dialog).not.toHaveBeenCalled();
310
+ expect(drawer).not.toHaveBeenCalled();
311
+ });
312
+
313
+ it('uses drawer uiMode when specified', async () => {
314
+ const engine = new FlowEngine();
315
+ const flowSettings = new FlowSettings(engine);
316
+ const TestFlowModel = createIsolatedFlowModel('test-4');
317
+ const model = new TestFlowModel({ uid: 'm-open-drawer', flowEngine: engine });
318
+
319
+ // Register one simple flow/step
320
+ TestFlowModel.registerFlow({
321
+ key: 'f',
322
+ steps: {
323
+ s: {
324
+ title: 'S',
325
+ uiSchema: {
326
+ field: { type: 'string', 'x-component': 'Input' },
327
+ },
328
+ },
329
+ },
330
+ });
331
+
332
+ // message stub
333
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
334
+
335
+ const openSpy = { lastTree: null as any };
336
+ const viewerDrawer = vi.fn(({ content }) => {
337
+ const dlg = { close: vi.fn(), Footer: (props: any) => null } as any;
338
+ openSpy.lastTree = typeof content === 'function' ? content(dlg, { defineMethod: vi.fn() }) : null;
339
+ return dlg;
340
+ });
341
+ const viewerDialog = vi.fn();
342
+ model.context.defineProperty('viewer', { value: { drawer: viewerDrawer, dialog: viewerDialog } });
343
+
344
+ await flowSettings.open({ model, flowKey: 'f', uiMode: 'drawer' } as any);
345
+ expect(viewerDrawer).toHaveBeenCalledTimes(1);
346
+ expect(viewerDialog).not.toHaveBeenCalled();
347
+ expect(openSpy.lastTree).toBeTruthy();
348
+ });
349
+
350
+ it('saves successfully and calls hooks, messages, and close', async () => {
351
+ const engine = new FlowEngine();
352
+ const flowSettings = new FlowSettings(engine);
353
+ const TestFlowModel = createIsolatedFlowModel('test-5');
354
+ const model = new TestFlowModel({ uid: 'm-open-save', flowEngine: engine });
355
+
356
+ const beforeHook = vi.fn();
357
+ const afterHook = vi.fn();
358
+ TestFlowModel.registerFlow({
359
+ key: 'fx',
360
+ steps: {
361
+ general: {
362
+ title: 'General',
363
+ defaultParams: { a: 1 },
364
+ beforeParamsSave: beforeHook,
365
+ afterParamsSave: afterHook,
366
+ uiSchema: {
367
+ field1: { type: 'string', 'x-component': 'Input' },
368
+ },
369
+ },
370
+ },
371
+ });
372
+
373
+ const info = vi.fn();
374
+ const error = vi.fn();
375
+ const success = vi.fn();
376
+ model.context.defineProperty('message', { value: { info, error, success } });
377
+
378
+ const setStepParams = vi.spyOn(model as any, 'setStepParams');
379
+ const save = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
380
+
381
+ // capture returned React tree for triggering primary button
382
+ let lastTree: any;
383
+ let lastDialog: any;
384
+ model.context.defineProperty('viewer', {
385
+ value: {
386
+ dialog: ({ content }) => {
387
+ lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
388
+ lastTree = typeof content === 'function' ? content(lastDialog) : null;
389
+ // return dialog object
390
+ return lastDialog;
391
+ },
392
+ drawer: ({ content }) => (model as any).context.viewer.dialog({ content }),
393
+ },
394
+ });
395
+
396
+ await flowSettings.open({ model, flowKey: 'fx', stepKey: 'general', uiMode: 'dialog' } as any);
397
+
398
+ // traverse the created React element tree to find the primary Button and click it
399
+ const primaryBtn = await findPrimaryButton(lastTree);
400
+ expect(primaryBtn).toBeTruthy();
401
+ await primaryBtn.props.onClick?.();
402
+
403
+ // assertions
404
+ expect(setStepParams).toHaveBeenCalledWith('fx', 'general', expect.any(Object));
405
+ expect(beforeHook).toHaveBeenCalled();
406
+ expect(save).toHaveBeenCalled();
407
+ expect(success).toHaveBeenCalled();
408
+ expect(afterHook).toHaveBeenCalled();
409
+ expect(lastDialog.close).toHaveBeenCalled();
410
+ });
411
+
412
+ it('calls onSaved callback after successful save', async () => {
413
+ const engine = new FlowEngine();
414
+ const flowSettings = new FlowSettings(engine);
415
+ const TestFlowModel = createIsolatedFlowModel('test-6');
416
+ const model = new TestFlowModel({ uid: 'm-open-onsaved', flowEngine: engine });
417
+
418
+ TestFlowModel.registerFlow({
419
+ key: 'flow',
420
+ steps: {
421
+ step: {
422
+ title: 'Step',
423
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
424
+ },
425
+ },
426
+ });
427
+
428
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
429
+ vi.spyOn(model as any, 'save').mockResolvedValue(undefined);
430
+
431
+ let lastTree: any;
432
+ let lastDialog: any;
433
+ model.context.defineProperty('viewer', {
434
+ value: {
435
+ dialog: ({ content }) => {
436
+ lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
437
+ lastTree = typeof content === 'function' ? content(lastDialog) : null;
438
+ return lastDialog;
439
+ },
440
+ },
441
+ });
442
+
443
+ const onSaved = vi.fn();
444
+ await flowSettings.open({ model, flowKey: 'flow', stepKey: 'step', onSaved } as any);
445
+
446
+ const primaryBtn = await findPrimaryButton(lastTree);
447
+ expect(primaryBtn).toBeTruthy();
448
+ await primaryBtn.props.onClick?.();
449
+
450
+ expect(onSaved).toHaveBeenCalledTimes(1);
451
+ expect(lastDialog.close).toHaveBeenCalled();
452
+ });
453
+
454
+ it('calls onCancel callback when cancel button clicked', async () => {
455
+ const engine = new FlowEngine();
456
+ const flowSettings = new FlowSettings(engine);
457
+ const TestFlowModel = createIsolatedFlowModel('test-7');
458
+ const model = new TestFlowModel({ uid: 'm-open-oncancel', flowEngine: engine });
459
+
460
+ TestFlowModel.registerFlow({
461
+ key: 'flow',
462
+ steps: {
463
+ step: {
464
+ title: 'Step',
465
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
466
+ },
467
+ },
468
+ });
469
+
470
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
471
+
472
+ let lastTree: any;
473
+ let lastDialog: any;
474
+ model.context.defineProperty('viewer', {
475
+ value: {
476
+ dialog: ({ content }) => {
477
+ lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
478
+ lastTree = typeof content === 'function' ? content(lastDialog) : null;
479
+ return lastDialog;
480
+ },
481
+ },
482
+ });
483
+
484
+ const onCancel = vi.fn();
485
+ await flowSettings.open({ model, flowKey: 'flow', stepKey: 'step', onCancel } as any);
486
+
487
+ const cancelBtn = await findCancelButton(lastTree);
488
+ expect(cancelBtn).toBeTruthy();
489
+ await cancelBtn.props.onClick?.();
490
+
491
+ expect(onCancel).toHaveBeenCalledTimes(1);
492
+ expect(lastDialog.close).toHaveBeenCalled();
493
+ });
494
+
495
+ it('handles save error by showing error message and keeping dialog open', async () => {
496
+ const engine = new FlowEngine();
497
+ const flowSettings = new FlowSettings(engine);
498
+ const TestFlowModel = createIsolatedFlowModel('test-8');
499
+ const model = new TestFlowModel({ uid: 'm-open-error', flowEngine: engine });
500
+
501
+ TestFlowModel.registerFlow({
502
+ key: 'fy',
503
+ steps: {
504
+ s: {
505
+ title: 'S',
506
+ defaultParams: { b: 2 },
507
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
508
+ },
509
+ },
510
+ });
511
+
512
+ const info = vi.fn();
513
+ const error = vi.fn();
514
+ const success = vi.fn();
515
+ model.context.defineProperty('message', { value: { info, error, success } });
516
+
517
+ vi.spyOn(model as any, 'saveStepParams').mockRejectedValue(new Error('boom'));
518
+
519
+ let lastTree: any;
520
+ let lastDialog: any;
521
+ model.context.defineProperty('viewer', {
522
+ value: {
523
+ dialog: ({ content }) => {
524
+ lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
525
+ lastTree = typeof content === 'function' ? content(lastDialog) : null;
526
+ return lastDialog;
527
+ },
528
+ },
529
+ });
530
+
531
+ await flowSettings.open({ model, flowKey: 'fy', stepKey: 's' } as any);
532
+
533
+ const primaryBtn = await findPrimaryButton(lastTree);
534
+ expect(primaryBtn).toBeTruthy();
535
+ await primaryBtn.props.onClick?.();
536
+
537
+ expect(error).toHaveBeenCalled();
538
+ expect(success).not.toHaveBeenCalled();
539
+ expect(lastDialog.close).not.toHaveBeenCalled();
540
+ });
541
+
542
+ it('filters steps by preset when preset=true', async () => {
543
+ const engine = new FlowEngine();
544
+ const flowSettings = new FlowSettings(engine);
545
+ const TestFlowModel = createIsolatedFlowModel('test-9');
546
+ const model = new TestFlowModel({ uid: 'm-open-preset', flowEngine: engine });
547
+
548
+ TestFlowModel.registerFlow({
549
+ key: 'pf',
550
+ steps: {
551
+ a: {
552
+ title: 'A',
553
+ preset: true,
554
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
555
+ },
556
+ b: {
557
+ title: 'B',
558
+ uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
559
+ },
560
+ },
561
+ });
562
+
563
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
564
+ const dialog = vi.fn(({ content }) => {
565
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
566
+ if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
567
+ return dlg;
568
+ });
569
+
570
+ model.context.defineProperty('viewer', {
571
+ value: {
572
+ dialog,
573
+ },
574
+ });
575
+
576
+ await flowSettings.open({ model, flowKey: 'pf', preset: true } as any);
577
+ expect(dialog).toHaveBeenCalled();
578
+ });
579
+
580
+ it('shows info when preset=true but no step is preset', async () => {
581
+ const engine = new FlowEngine();
582
+ const flowSettings = new FlowSettings(engine);
583
+ const TestFlowModel = createIsolatedFlowModel('test-10');
584
+ const model = new TestFlowModel({ uid: 'm-open-preset-none', flowEngine: engine });
585
+
586
+ TestFlowModel.registerFlow({
587
+ key: 'pf2',
588
+ steps: {
589
+ x: { title: 'X', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } },
590
+ y: { title: 'Y', uiSchema: { g: { type: 'string', 'x-component': 'Input' } } },
591
+ },
592
+ });
593
+
594
+ const info = vi.fn();
595
+ const dialog = vi.fn();
596
+ model.context.defineProperty('message', { value: { info, error: vi.fn(), success: vi.fn() } });
597
+ model.context.defineProperty('viewer', { value: { dialog } });
598
+
599
+ await flowSettings.open({ model, flowKey: 'pf2', preset: true } as any);
600
+ expect(info).not.toHaveBeenCalled(); // 这种一般是在添加 sub model 的场景调用的,如果为空应该直接忽略,不需要 info 提示
601
+ expect(dialog).not.toHaveBeenCalled();
602
+ });
603
+
604
+ it('shows dialog when preset=true and step has hideInSettings=true', async () => {
605
+ const engine = new FlowEngine();
606
+ const flowSettings = new FlowSettings(engine);
607
+ const model = new FlowModel({ uid: 'm-open-preset-hidden', flowEngine: engine });
608
+
609
+ const M = model.constructor as any;
610
+ M.registerFlow({
611
+ key: 'pf3',
612
+ steps: {
613
+ hiddenStep: {
614
+ title: 'Hidden Step',
615
+ preset: true,
616
+ hideInSettings: true,
617
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
618
+ },
619
+ visibleStep: {
620
+ title: 'Visible Step',
621
+ uiSchema: { field2: { type: 'string', 'x-component': 'Input' } },
622
+ },
623
+ },
624
+ });
625
+
626
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
627
+ const dialog = vi.fn(({ content }) => {
628
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
629
+ if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
630
+ return dlg;
631
+ });
632
+
633
+ model.context.defineProperty('viewer', { value: { dialog } });
634
+
635
+ await flowSettings.open({ model, flowKey: 'pf3', preset: true } as any);
636
+ expect(dialog).toHaveBeenCalled(); // 应该显示弹窗,因为 hiddenStep 有 preset=true
637
+ });
638
+
639
+ it('ignores hideInSettings when preset=true for individual step', async () => {
640
+ const engine = new FlowEngine();
641
+ const flowSettings = new FlowSettings(engine);
642
+ const model = new FlowModel({ uid: 'm-open-preset-single-hidden', flowEngine: engine });
643
+
644
+ const M = model.constructor as any;
645
+ M.registerFlow({
646
+ key: 'pf4',
647
+ steps: {
648
+ targetStep: {
649
+ title: 'Target Step',
650
+ preset: true,
651
+ hideInSettings: true,
652
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
653
+ },
654
+ },
655
+ });
656
+
657
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
658
+ const dialog = vi.fn(({ content }) => {
659
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
660
+ if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
661
+ return dlg;
662
+ });
663
+
664
+ model.context.defineProperty('viewer', { value: { dialog } });
665
+
666
+ await flowSettings.open({
667
+ model,
668
+ flowKey: 'pf4',
669
+ stepKey: 'targetStep',
670
+ preset: true,
671
+ } as any);
672
+
673
+ expect(dialog).toHaveBeenCalled(); // 应该显示弹窗,即使 hideInSettings=true
674
+ });
675
+
676
+ it('respects hideInSettings when preset=false', async () => {
677
+ const engine = new FlowEngine();
678
+ const flowSettings = new FlowSettings(engine);
679
+ const model = new FlowModel({ uid: 'm-open-normal-hidden', flowEngine: engine });
680
+
681
+ const M = model.constructor as any;
682
+ M.registerFlow({
683
+ key: 'pf5',
684
+ steps: {
685
+ hiddenStep: {
686
+ title: 'Hidden Step',
687
+ hideInSettings: true,
688
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
689
+ },
690
+ visibleStep: {
691
+ title: 'Visible Step',
692
+ uiSchema: { field2: { type: 'string', 'x-component': 'Input' } },
693
+ },
694
+ },
695
+ });
696
+
697
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
698
+ const dialog = vi.fn(({ content }) => {
699
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
700
+ if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
701
+ return dlg;
702
+ });
703
+
704
+ model.context.defineProperty('viewer', { value: { dialog } });
705
+
706
+ await flowSettings.open({ model, flowKey: 'pf5', preset: false } as any);
707
+ expect(dialog).toHaveBeenCalled(); // 应该显示弹窗,但只包含 visibleStep
708
+ });
709
+
710
+ it('accepts uiMode object (dialog) and merges props while keeping our content', async () => {
711
+ const engine = new FlowEngine();
712
+ const flowSettings = new FlowSettings(engine);
713
+ const TestFlowModel = createIsolatedFlowModel('test-11');
714
+ const model = new TestFlowModel({ uid: 'm-open-uiMode-obj-dialog', flowEngine: engine });
715
+
716
+ TestFlowModel.registerFlow({
717
+ key: 'flowObj',
718
+ steps: {
719
+ step: {
720
+ title: 'Step',
721
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
722
+ },
723
+ },
724
+ });
725
+
726
+ const info = vi.fn();
727
+ const error = vi.fn();
728
+ const success = vi.fn();
729
+ model.context.defineProperty('message', { value: { info, error, success } });
730
+
731
+ const sentinelContent = vi.fn();
732
+ const viewerDialog = vi.fn((opts: any) => {
733
+ // title/width should come from props
734
+ expect(opts.title).toBe('Custom title');
735
+ expect(opts.width).toBe(1024);
736
+ // extra props should pass through
737
+ expect(opts.maskClosable).toBe(false);
738
+ // our content should override incoming props.content
739
+ expect(opts.content).not.toBe(sentinelContent);
740
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
741
+ // Execute content once to ensure it is callable
742
+ if (typeof opts.content === 'function') {
743
+ opts.content(dlg, { defineMethod: vi.fn() });
744
+ }
745
+ return dlg;
746
+ });
747
+
748
+ model.context.defineProperty('viewer', { value: { dialog: viewerDialog } });
749
+
750
+ await flowSettings.open({
751
+ model,
752
+ flowKey: 'flowObj',
753
+ stepKey: 'step',
754
+ uiMode: {
755
+ type: 'dialog',
756
+ props: { title: 'Custom title', width: 1024, maskClosable: false, content: sentinelContent },
757
+ },
758
+ } as any);
759
+
760
+ expect(viewerDialog).toHaveBeenCalledTimes(1);
761
+ });
762
+
763
+ it('accepts uiMode object with type drawer and calls viewer.drawer', async () => {
764
+ const engine = new FlowEngine();
765
+ const flowSettings = new FlowSettings(engine);
766
+ const TestFlowModel = createIsolatedFlowModel('test-12');
767
+ const model = new TestFlowModel({ uid: 'm-open-uiMode-obj-drawer', flowEngine: engine });
768
+
769
+ TestFlowModel.registerFlow({
770
+ key: 'flowObj2',
771
+ steps: {
772
+ step: {
773
+ title: 'Step',
774
+ uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
775
+ },
776
+ },
777
+ });
778
+
779
+ const drawer = vi.fn((opts: any) => {
780
+ // also check title fallback if not provided in props
781
+ expect(typeof opts.title === 'string').toBe(true);
782
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
783
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
784
+ return dlg;
785
+ });
786
+ const dialog = vi.fn();
787
+
788
+ model.context.defineProperty('viewer', { value: { drawer, dialog } });
789
+
790
+ await flowSettings.open({
791
+ model,
792
+ flowKey: 'flowObj2',
793
+ stepKey: 'step',
794
+ uiMode: { type: 'drawer', props: {} },
795
+ } as any);
796
+
797
+ expect(drawer).toHaveBeenCalledTimes(1);
798
+ expect(dialog).not.toHaveBeenCalled();
799
+ });
800
+
801
+ it('sets title to step title when flowKey+stepKey are provided and only one step matches', async () => {
802
+ const engine = new FlowEngine();
803
+ const flowSettings = new FlowSettings(engine);
804
+ const TestFlowModel = createIsolatedFlowModel('test-13');
805
+ const model = new TestFlowModel({ uid: 'm-open-title-step', flowEngine: engine });
806
+
807
+ TestFlowModel.registerFlow({
808
+ key: 'tf-step',
809
+ steps: {
810
+ general: {
811
+ title: 'General',
812
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
813
+ },
814
+ },
815
+ });
816
+
817
+ const dialog = vi.fn((opts: any) => {
818
+ expect(opts.title).toBe('General');
819
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
820
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
821
+ return dlg;
822
+ });
823
+ model.context.defineProperty('viewer', { value: { dialog } });
824
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
825
+
826
+ await flowSettings.open({ model, flowKey: 'tf-step', stepKey: 'general' } as any);
827
+ expect(dialog).toHaveBeenCalledTimes(1);
828
+ });
829
+
830
+ it('sets title to flow title when only one flow and no stepKey', async () => {
831
+ const engine = new FlowEngine();
832
+ const flowSettings = new FlowSettings(engine);
833
+ const TestFlowModel = createIsolatedFlowModel('test-14');
834
+ const model = new TestFlowModel({ uid: 'm-open-title-flow', flowEngine: engine });
835
+
836
+ TestFlowModel.registerFlow({
837
+ key: 'tf-flow',
838
+ title: 'My Flow',
839
+ steps: {
840
+ a: { title: 'A', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } },
841
+ b: { title: 'B', uiSchema: { g: { type: 'string', 'x-component': 'Input' } } },
842
+ },
843
+ });
844
+
845
+ const dialog = vi.fn((opts: any) => {
846
+ expect(opts.title).toBe('A');
847
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
848
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
849
+ return dlg;
850
+ });
851
+ model.context.defineProperty('viewer', { value: { dialog } });
852
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
853
+
854
+ // explicitly pass flowKey to avoid interference from other tests
855
+ await flowSettings.open({ model, flowKey: 'tf-flow' } as any);
856
+ expect(dialog).toHaveBeenCalledTimes(1);
857
+ });
858
+
859
+ it('sets empty title when rendering multiple flows together', async () => {
860
+ const engine = new FlowEngine();
861
+ const flowSettings = new FlowSettings(engine);
862
+ const TestFlowModel = createIsolatedFlowModel('test-15');
863
+ const model = new TestFlowModel({ uid: 'm-open-title-empty', flowEngine: engine });
864
+
865
+ TestFlowModel.registerFlow({
866
+ key: 'f1',
867
+ title: 'Flow 1',
868
+ steps: { s1: { title: 'S1', uiSchema: { a: { type: 'string', 'x-component': 'Input' } } } },
869
+ });
870
+ TestFlowModel.registerFlow({
871
+ key: 'f2',
872
+ title: 'Flow 2',
873
+ steps: { s2: { title: 'S2', uiSchema: { b: { type: 'string', 'x-component': 'Input' } } } },
874
+ });
875
+
876
+ const dialog = vi.fn((opts: any) => {
877
+ expect(opts.title).toBe('');
878
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
879
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
880
+ return dlg;
881
+ });
882
+ model.context.defineProperty('viewer', { value: { dialog } });
883
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
884
+
885
+ // omit flowKey to include all flows
886
+ await flowSettings.open({ model } as any);
887
+ expect(dialog).toHaveBeenCalledTimes(1);
888
+ });
889
+
890
+ it('resolves function-based step uiMode when single step is rendered', async () => {
891
+ const engine = new FlowEngine();
892
+ const flowSettings = new FlowSettings(engine);
893
+ const model = new FlowModel({ uid: 'm-step-uimode-function', flowEngine: engine });
894
+
895
+ const M = model.constructor as any;
896
+ M.registerFlow({
897
+ key: 'flowWithFunctionUiMode',
898
+ steps: {
899
+ step: {
900
+ title: 'Step',
901
+ uiMode: (ctx: any) => ({ type: 'drawer', props: { title: 'Function Title', width: 800 } }),
902
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
903
+ },
904
+ },
905
+ });
906
+
907
+ const drawer = vi.fn((opts: any) => {
908
+ expect(opts.title).toBe('Function Title');
909
+ expect(opts.width).toBe(800);
910
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
911
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
912
+ return dlg;
913
+ });
914
+ const dialog = vi.fn();
915
+
916
+ model.context.defineProperty('viewer', { value: { drawer, dialog } });
917
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
918
+
919
+ await flowSettings.open({ model, flowKey: 'flowWithFunctionUiMode', stepKey: 'step' } as any);
920
+
921
+ expect(drawer).toHaveBeenCalledTimes(1);
922
+ expect(dialog).not.toHaveBeenCalled();
923
+ });
924
+
925
+ it('resolves async function-based step uiMode when single step is rendered', async () => {
926
+ const engine = new FlowEngine();
927
+ const flowSettings = new FlowSettings(engine);
928
+ const model = new FlowModel({ uid: 'm-step-uimode-async-function', flowEngine: engine });
929
+
930
+ const M = model.constructor as any;
931
+ M.registerFlow({
932
+ key: 'flowWithAsyncFunctionUiMode',
933
+ steps: {
934
+ step: {
935
+ title: 'Step',
936
+ uiMode: async (ctx: any) => {
937
+ await new Promise((resolve) => setTimeout(resolve, 10));
938
+ return { type: 'dialog', props: { title: 'Async Function Title', width: 900 } };
939
+ },
940
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
941
+ },
942
+ },
943
+ });
944
+
945
+ const dialog = vi.fn((opts: any) => {
946
+ expect(opts.title).toBe('Async Function Title');
947
+ expect(opts.width).toBe(900);
948
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
949
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
950
+ return dlg;
951
+ });
952
+
953
+ model.context.defineProperty('viewer', { value: { dialog } });
954
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
955
+
956
+ await flowSettings.open({ model, flowKey: 'flowWithAsyncFunctionUiMode', stepKey: 'step' } as any);
957
+
958
+ expect(dialog).toHaveBeenCalledTimes(1);
959
+ });
960
+
961
+ it('uses step static uiMode object when single step is rendered', async () => {
962
+ const engine = new FlowEngine();
963
+ const flowSettings = new FlowSettings(engine);
964
+ const model = new FlowModel({ uid: 'm-step-uimode-static', flowEngine: engine });
965
+
966
+ const M = model.constructor as any;
967
+ M.registerFlow({
968
+ key: 'flowWithStaticUiMode',
969
+ steps: {
970
+ step: {
971
+ title: 'Step',
972
+ uiMode: { type: 'drawer', props: { title: 'Static Title', width: 700 } },
973
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
974
+ },
975
+ },
976
+ });
977
+
978
+ const drawer = vi.fn((opts: any) => {
979
+ expect(opts.title).toBe('Static Title');
980
+ expect(opts.width).toBe(700);
981
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
982
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
983
+ return dlg;
984
+ });
985
+ const dialog = vi.fn();
986
+
987
+ model.context.defineProperty('viewer', { value: { drawer, dialog } });
988
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
989
+
990
+ await flowSettings.open({ model, flowKey: 'flowWithStaticUiMode', stepKey: 'step' } as any);
991
+
992
+ expect(drawer).toHaveBeenCalledTimes(1);
993
+ expect(dialog).not.toHaveBeenCalled();
994
+ });
995
+
996
+ it('fallbacks to global uiMode when step has no uiMode and single step is rendered', async () => {
997
+ const engine = new FlowEngine();
998
+ const flowSettings = new FlowSettings(engine);
999
+ const model = new FlowModel({ uid: 'm-step-uimode-fallback', flowEngine: engine });
1000
+
1001
+ const M = model.constructor as any;
1002
+ M.registerFlow({
1003
+ key: 'flowWithoutStepUiMode',
1004
+ steps: {
1005
+ step: {
1006
+ title: 'Step',
1007
+ // no uiMode defined at step level
1008
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1009
+ },
1010
+ },
1011
+ });
1012
+
1013
+ const drawer = vi.fn((opts: any) => {
1014
+ expect(opts.title).toBe('Global Title');
1015
+ expect(opts.width).toBe(600);
1016
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
1017
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
1018
+ return dlg;
1019
+ });
1020
+ const dialog = vi.fn();
1021
+
1022
+ model.context.defineProperty('viewer', { value: { drawer, dialog } });
1023
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1024
+
1025
+ await flowSettings.open({
1026
+ model,
1027
+ flowKey: 'flowWithoutStepUiMode',
1028
+ stepKey: 'step',
1029
+ uiMode: { type: 'drawer', props: { title: 'Global Title', width: 600 } },
1030
+ } as any);
1031
+
1032
+ expect(drawer).toHaveBeenCalledTimes(1);
1033
+ expect(dialog).not.toHaveBeenCalled();
1034
+ });
1035
+
1036
+ it('ignores step uiMode when multiple steps are rendered', async () => {
1037
+ const engine = new FlowEngine();
1038
+ const flowSettings = new FlowSettings(engine);
1039
+ const model = new FlowModel({ uid: 'm-multi-step-ignore-uimode', flowEngine: engine });
1040
+
1041
+ const M = model.constructor as any;
1042
+ M.registerFlow({
1043
+ key: 'flowWithMultiSteps',
1044
+ steps: {
1045
+ step1: {
1046
+ title: 'Step1',
1047
+ uiMode: { type: 'drawer', props: { title: 'Should be ignored', width: 999 } },
1048
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1049
+ },
1050
+ step2: {
1051
+ title: 'Step2',
1052
+ uiMode: (ctx: any) => ({ type: 'drawer', props: { title: 'Should also be ignored', width: 888 } }),
1053
+ uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
1054
+ },
1055
+ },
1056
+ });
1057
+
1058
+ const dialog = vi.fn((opts: any) => {
1059
+ // Should use global uiMode setting, not step-level uiMode
1060
+ expect(opts.title).toBe('Global Multi Steps Title');
1061
+ expect(opts.width).toBe(1000);
1062
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
1063
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
1064
+ return dlg;
1065
+ });
1066
+ const drawer = vi.fn();
1067
+
1068
+ model.context.defineProperty('viewer', { value: { dialog, drawer } });
1069
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1070
+
1071
+ await flowSettings.open({
1072
+ model,
1073
+ flowKey: 'flowWithMultiSteps',
1074
+ uiMode: { type: 'dialog', props: { title: 'Global Multi Steps Title', width: 1000 } },
1075
+ } as any);
1076
+
1077
+ expect(dialog).toHaveBeenCalledTimes(1);
1078
+ expect(drawer).not.toHaveBeenCalled();
1079
+ });
1080
+
1081
+ it('uses embed uiMode with target element and callbacks', async () => {
1082
+ const engine = new FlowEngine();
1083
+ const flowSettings = new FlowSettings(engine);
1084
+ const model = new FlowModel({ uid: 'm-embed-mode', flowEngine: engine });
1085
+
1086
+ // Create mock DOM element for embed target
1087
+ const mockTarget = document.createElement('div');
1088
+ mockTarget.id = 'nocobase-embed-container';
1089
+ mockTarget.style.width = 'auto';
1090
+ mockTarget.style.maxWidth = 'none';
1091
+ document.body.appendChild(mockTarget);
1092
+
1093
+ // Mock querySelector to return our mock element
1094
+ const originalQuerySelector = document.querySelector;
1095
+ document.querySelector = vi.fn((selector) => {
1096
+ if (selector === '#nocobase-embed-container') {
1097
+ return mockTarget;
1098
+ }
1099
+ return originalQuerySelector.call(document, selector);
1100
+ });
1101
+
1102
+ const M = model.constructor as any;
1103
+ M.registerFlow({
1104
+ key: 'embedFlow',
1105
+ steps: {
1106
+ step: {
1107
+ title: 'Step',
1108
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1109
+ },
1110
+ },
1111
+ });
1112
+
1113
+ const onOpenSpy = vi.fn();
1114
+ const onCloseSpy = vi.fn();
1115
+ const embed = vi.fn((opts: any) => {
1116
+ expect(opts.target).toBe(mockTarget);
1117
+ expect(opts.width).toBe('60%');
1118
+ expect(opts.maxWidth).toBe('900px');
1119
+ expect(typeof opts.onOpen).toBe('function');
1120
+ expect(typeof opts.onClose).toBe('function');
1121
+
1122
+ // Test onOpen callback
1123
+ opts.onOpen();
1124
+ expect(mockTarget.style.width).toBe('60%');
1125
+ expect(mockTarget.style.maxWidth).toBe('900px');
1126
+ expect(onOpenSpy).toHaveBeenCalled();
1127
+
1128
+ // Test onClose callback
1129
+ opts.onClose();
1130
+ expect(mockTarget.style.width).toBe('auto');
1131
+ expect(mockTarget.style.maxWidth).toBe('none');
1132
+ expect(onCloseSpy).toHaveBeenCalled();
1133
+
1134
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
1135
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
1136
+ return dlg;
1137
+ });
1138
+ const dialog = vi.fn();
1139
+ const drawer = vi.fn();
1140
+
1141
+ model.context.defineProperty('viewer', { value: { embed, dialog, drawer } });
1142
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1143
+
1144
+ await flowSettings.open({
1145
+ model,
1146
+ flowKey: 'embedFlow',
1147
+ stepKey: 'step',
1148
+ uiMode: {
1149
+ type: 'embed',
1150
+ props: {
1151
+ width: '60%',
1152
+ maxWidth: '900px',
1153
+ onOpen: onOpenSpy,
1154
+ onClose: onCloseSpy,
1155
+ },
1156
+ },
1157
+ } as any);
1158
+
1159
+ expect(embed).toHaveBeenCalledTimes(1);
1160
+ expect(dialog).not.toHaveBeenCalled();
1161
+ expect(drawer).not.toHaveBeenCalled();
1162
+
1163
+ // Cleanup
1164
+ document.body.removeChild(mockTarget);
1165
+ document.querySelector = originalQuerySelector;
1166
+ });
1167
+
1168
+ it('uses embed uiMode with default props when target element exists', async () => {
1169
+ const engine = new FlowEngine();
1170
+ const flowSettings = new FlowSettings(engine);
1171
+ const model = new FlowModel({ uid: 'm-embed-default', flowEngine: engine });
1172
+
1173
+ // Create mock DOM element for embed target
1174
+ const mockTarget = document.createElement('div');
1175
+ mockTarget.id = 'nocobase-embed-container';
1176
+ document.body.appendChild(mockTarget);
1177
+
1178
+ // Mock querySelector
1179
+ const originalQuerySelector = document.querySelector;
1180
+ document.querySelector = vi.fn((selector) => {
1181
+ if (selector === '#nocobase-embed-container') {
1182
+ return mockTarget;
1183
+ }
1184
+ return originalQuerySelector.call(document, selector);
1185
+ });
1186
+
1187
+ const M = model.constructor as any;
1188
+ M.registerFlow({
1189
+ key: 'embedDefaultFlow',
1190
+ steps: {
1191
+ step: {
1192
+ title: 'Step',
1193
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1194
+ },
1195
+ },
1196
+ });
1197
+
1198
+ const embed = vi.fn((opts: any) => {
1199
+ expect(opts.target).toBe(mockTarget);
1200
+ // Test default width and maxWidth
1201
+ opts.onOpen();
1202
+ expect(mockTarget.style.width).toBe('33.3%'); // default width
1203
+ expect(mockTarget.style.maxWidth).toBe('800px'); // default maxWidth
1204
+
1205
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
1206
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
1207
+ return dlg;
1208
+ });
1209
+
1210
+ model.context.defineProperty('viewer', { value: { embed } });
1211
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1212
+
1213
+ await flowSettings.open({
1214
+ model,
1215
+ flowKey: 'embedDefaultFlow',
1216
+ stepKey: 'step',
1217
+ uiMode: 'embed',
1218
+ } as any);
1219
+
1220
+ expect(embed).toHaveBeenCalledTimes(1);
1221
+
1222
+ // Cleanup
1223
+ document.body.removeChild(mockTarget);
1224
+ document.querySelector = originalQuerySelector;
1225
+ });
1226
+
1227
+ it('handles embed uiMode when target element is not found', async () => {
1228
+ const engine = new FlowEngine();
1229
+ const flowSettings = new FlowSettings(engine);
1230
+ const model = new FlowModel({ uid: 'm-embed-no-target', flowEngine: engine });
1231
+
1232
+ // Mock querySelector to return null (target not found)
1233
+ const originalQuerySelector = document.querySelector;
1234
+ document.querySelector = vi.fn(() => null);
1235
+
1236
+ const M = model.constructor as any;
1237
+ M.registerFlow({
1238
+ key: 'embedNoTargetFlow',
1239
+ steps: {
1240
+ step: {
1241
+ title: 'Step',
1242
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1243
+ },
1244
+ },
1245
+ });
1246
+
1247
+ const embed = vi.fn((opts: any) => {
1248
+ expect(opts.target).toBeNull();
1249
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
1250
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
1251
+ return dlg;
1252
+ });
1253
+
1254
+ model.context.defineProperty('viewer', { value: { embed } });
1255
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1256
+
1257
+ await flowSettings.open({
1258
+ model,
1259
+ flowKey: 'embedNoTargetFlow',
1260
+ stepKey: 'step',
1261
+ uiMode: 'embed',
1262
+ } as any);
1263
+
1264
+ expect(embed).toHaveBeenCalledTimes(1);
1265
+
1266
+ // Restore querySelector
1267
+ document.querySelector = originalQuerySelector;
1268
+ });
1269
+
1270
+ it('handles error in function-based step uiMode gracefully', async () => {
1271
+ const engine = new FlowEngine();
1272
+ const flowSettings = new FlowSettings(engine);
1273
+ const model = new FlowModel({ uid: 'm-step-uimode-error', flowEngine: engine });
1274
+
1275
+ const M = model.constructor as any;
1276
+ M.registerFlow({
1277
+ key: 'flowWithErrorUiMode',
1278
+ steps: {
1279
+ step: {
1280
+ title: 'Step',
1281
+ uiMode: (ctx: any) => {
1282
+ throw new Error('uiMode function error');
1283
+ },
1284
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1285
+ },
1286
+ },
1287
+ });
1288
+
1289
+ // Mock console.error to avoid log noise during test
1290
+ const originalConsoleError = console.error;
1291
+ console.error = vi.fn();
1292
+
1293
+ const dialog = vi.fn((opts: any) => {
1294
+ // Should fallback to default 'dialog' when function throws error
1295
+ expect(typeof opts.title === 'string').toBe(true);
1296
+ const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
1297
+ if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
1298
+ return dlg;
1299
+ });
1300
+
1301
+ model.context.defineProperty('viewer', { value: { dialog } });
1302
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1303
+
1304
+ await flowSettings.open({ model, flowKey: 'flowWithErrorUiMode', stepKey: 'step' } as any);
1305
+
1306
+ expect(dialog).toHaveBeenCalledTimes(1);
1307
+ expect(console.error).toHaveBeenCalledWith('Error resolving uiMode function:', expect.any(Error));
1308
+
1309
+ // Restore console.error
1310
+ console.error = originalConsoleError;
1311
+ });
1312
+
1313
+ it('supports reactive objects in uiMode function and auto-updates dialog props', async () => {
1314
+ const { observable } = await import('@formily/reactive');
1315
+
1316
+ const engine = new FlowEngine();
1317
+ const flowSettings = new FlowSettings(engine);
1318
+ const model = new FlowModel({ uid: 'm-reactive-uimode', flowEngine: engine });
1319
+
1320
+ // Create reactive state object
1321
+ const reactiveState = observable({
1322
+ title: 'Initial Title',
1323
+ width: 600,
1324
+ });
1325
+
1326
+ const M = model.constructor as any;
1327
+ M.registerFlow({
1328
+ key: 'flowWithReactiveUiMode',
1329
+ steps: {
1330
+ step: {
1331
+ title: 'Step',
1332
+ uiMode: (ctx: any) => ({
1333
+ type: 'dialog',
1334
+ props: {
1335
+ title: reactiveState.title,
1336
+ width: reactiveState.width,
1337
+ },
1338
+ }),
1339
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1340
+ },
1341
+ },
1342
+ });
1343
+
1344
+ const updateSpy = vi.fn();
1345
+ const closeSpy = vi.fn();
1346
+ const dialog = vi.fn((opts: any) => {
1347
+ // Verify initial props
1348
+ expect(opts.title).toBe('Initial Title');
1349
+ expect(opts.width).toBe(600);
1350
+
1351
+ const dlg = {
1352
+ close: closeSpy,
1353
+ Footer: (p: any) => null,
1354
+ update: updateSpy,
1355
+ } as any;
1356
+
1357
+ if (typeof opts.content === 'function') {
1358
+ opts.content(dlg, { defineMethod: vi.fn() });
1359
+ }
1360
+ return dlg;
1361
+ });
1362
+
1363
+ model.context.defineProperty('viewer', { value: { dialog } });
1364
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1365
+
1366
+ await flowSettings.open({ model, flowKey: 'flowWithReactiveUiMode', stepKey: 'step' } as any);
1367
+
1368
+ expect(dialog).toHaveBeenCalledTimes(1);
1369
+
1370
+ // Wait for autorun to be setup
1371
+ await new Promise((resolve) => setTimeout(resolve, 10));
1372
+
1373
+ // Update reactive state
1374
+ reactiveState.title = 'Updated Title';
1375
+ reactiveState.width = 800;
1376
+
1377
+ // Wait for reactive update
1378
+ await new Promise((resolve) => setTimeout(resolve, 10));
1379
+
1380
+ // Verify that dialog.update was called with new props
1381
+ expect(updateSpy).toHaveBeenCalledWith({
1382
+ title: 'Updated Title',
1383
+ width: 800,
1384
+ });
1385
+ });
1386
+
1387
+ it('properly disposes reactive listener when dialog is closed', async () => {
1388
+ const { observable } = await import('@formily/reactive');
1389
+
1390
+ const engine = new FlowEngine();
1391
+ const flowSettings = new FlowSettings(engine);
1392
+ const model = new FlowModel({ uid: 'm-reactive-dispose', flowEngine: engine });
1393
+
1394
+ const reactiveState = observable({
1395
+ title: 'Title',
1396
+ width: 500,
1397
+ });
1398
+
1399
+ const M = model.constructor as any;
1400
+ M.registerFlow({
1401
+ key: 'flowWithReactiveDispose',
1402
+ steps: {
1403
+ step: {
1404
+ title: 'Step',
1405
+ uiMode: (ctx: any) => ({
1406
+ type: 'dialog',
1407
+ props: {
1408
+ title: reactiveState.title,
1409
+ width: reactiveState.width,
1410
+ },
1411
+ }),
1412
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1413
+ },
1414
+ },
1415
+ });
1416
+
1417
+ let onCloseFn: Function | undefined;
1418
+ const updateSpy = vi.fn();
1419
+ const dialog = vi.fn((opts: any) => {
1420
+ // Capture the onClose callback
1421
+ onCloseFn = opts.onClose;
1422
+
1423
+ const dlg = {
1424
+ close: vi.fn(),
1425
+ Footer: (p: any) => null,
1426
+ update: updateSpy,
1427
+ } as any;
1428
+
1429
+ if (typeof opts.content === 'function') {
1430
+ opts.content(dlg, { defineMethod: vi.fn() });
1431
+ }
1432
+ return dlg;
1433
+ });
1434
+
1435
+ model.context.defineProperty('viewer', { value: { dialog } });
1436
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1437
+
1438
+ await flowSettings.open({ model, flowKey: 'flowWithReactiveDispose', stepKey: 'step' } as any);
1439
+
1440
+ expect(dialog).toHaveBeenCalledTimes(1);
1441
+ expect(typeof onCloseFn).toBe('function');
1442
+
1443
+ // Wait for autorun to be setup
1444
+ await new Promise((resolve) => setTimeout(resolve, 10));
1445
+
1446
+ // Call the dispose function to simulate dialog close
1447
+ onCloseFn?.();
1448
+
1449
+ // Update reactive state after disposal
1450
+ reactiveState.title = 'Should Not Update';
1451
+ reactiveState.width = 999;
1452
+
1453
+ // Wait to ensure no update occurs
1454
+ await new Promise((resolve) => setTimeout(resolve, 100));
1455
+
1456
+ // Verify that dialog.update was NOT called after disposal
1457
+ expect(updateSpy).not.toHaveBeenCalled();
1458
+ });
1459
+
1460
+ it('handles reactive uiMode when rendering multiple steps (should ignore step-level reactive uiMode)', async () => {
1461
+ const { observable } = await import('@formily/reactive');
1462
+
1463
+ const engine = new FlowEngine();
1464
+ const flowSettings = new FlowSettings(engine);
1465
+ const model = new FlowModel({ uid: 'm-multi-reactive', flowEngine: engine });
1466
+
1467
+ const reactiveState = observable({
1468
+ title: 'Reactive Title',
1469
+ width: 700,
1470
+ });
1471
+
1472
+ const M = model.constructor as any;
1473
+ M.registerFlow({
1474
+ key: 'flowWithMultiStepsReactive',
1475
+ steps: {
1476
+ step1: {
1477
+ title: 'Step1',
1478
+ uiMode: (ctx: any) => ({
1479
+ type: 'dialog',
1480
+ props: {
1481
+ title: reactiveState.title,
1482
+ width: reactiveState.width,
1483
+ },
1484
+ }),
1485
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1486
+ },
1487
+ step2: {
1488
+ title: 'Step2',
1489
+ uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
1490
+ },
1491
+ },
1492
+ });
1493
+
1494
+ const dialog = vi.fn((opts: any) => {
1495
+ // When multiple steps, should use global uiMode, not step-level reactive uiMode
1496
+ expect(opts.title).toBe('Global Title');
1497
+ expect(opts.width).toBe(1000);
1498
+
1499
+ const dlg = {
1500
+ close: vi.fn(),
1501
+ Footer: (p: any) => null,
1502
+ update: vi.fn(),
1503
+ } as any;
1504
+
1505
+ if (typeof opts.content === 'function') {
1506
+ opts.content(dlg, { defineMethod: vi.fn() });
1507
+ }
1508
+ return dlg;
1509
+ });
1510
+
1511
+ model.context.defineProperty('viewer', { value: { dialog } });
1512
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1513
+
1514
+ await flowSettings.open({
1515
+ model,
1516
+ flowKey: 'flowWithMultiStepsReactive',
1517
+ uiMode: { type: 'dialog', props: { title: 'Global Title', width: 1000 } },
1518
+ } as any);
1519
+
1520
+ expect(dialog).toHaveBeenCalledTimes(1);
1521
+ });
1522
+
1523
+ it('handles async reactive uiMode function updates', async () => {
1524
+ const { observable } = await import('@formily/reactive');
1525
+
1526
+ const engine = new FlowEngine();
1527
+ const flowSettings = new FlowSettings(engine);
1528
+ const model = new FlowModel({ uid: 'm-async-reactive', flowEngine: engine });
1529
+
1530
+ const reactiveState = observable({
1531
+ title: 'Async Title',
1532
+ width: 650,
1533
+ });
1534
+
1535
+ const M = model.constructor as any;
1536
+ M.registerFlow({
1537
+ key: 'flowWithAsyncReactiveUiMode',
1538
+ steps: {
1539
+ step: {
1540
+ title: 'Step',
1541
+ uiMode: async (ctx: any) => {
1542
+ return {
1543
+ type: 'dialog',
1544
+ props: {
1545
+ title: reactiveState.title,
1546
+ width: reactiveState.width,
1547
+ },
1548
+ };
1549
+ },
1550
+ uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
1551
+ },
1552
+ },
1553
+ });
1554
+
1555
+ const updateSpy = vi.fn();
1556
+ const dialog = vi.fn((opts: any) => {
1557
+ expect(opts.title).toBe('Async Title');
1558
+ expect(opts.width).toBe(650);
1559
+
1560
+ const dlg = {
1561
+ close: vi.fn(),
1562
+ Footer: (p: any) => null,
1563
+ update: updateSpy,
1564
+ } as any;
1565
+
1566
+ if (typeof opts.content === 'function') {
1567
+ opts.content(dlg, { defineMethod: vi.fn() });
1568
+ }
1569
+ return dlg;
1570
+ });
1571
+
1572
+ model.context.defineProperty('viewer', { value: { dialog } });
1573
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1574
+
1575
+ await flowSettings.open({ model, flowKey: 'flowWithAsyncReactiveUiMode', stepKey: 'step' } as any);
1576
+
1577
+ expect(dialog).toHaveBeenCalledTimes(1);
1578
+
1579
+ // Wait for autorun and async uiMode resolution
1580
+ await new Promise((resolve) => setTimeout(resolve, 20));
1581
+
1582
+ // Update reactive state
1583
+ reactiveState.title = 'Async Updated Title';
1584
+ reactiveState.width = 750;
1585
+
1586
+ // Wait for reactive update
1587
+ await new Promise((resolve) => setTimeout(resolve, 100));
1588
+
1589
+ // Verify that dialog.update was called with new props
1590
+ expect(updateSpy).toHaveBeenCalledWith({
1591
+ title: 'Async Updated Title',
1592
+ width: 750,
1593
+ });
1594
+ });
1595
+
1596
+ // =============================
1597
+ // Tests for currentDialog.submit method assignment
1598
+ // =============================
1599
+
1600
+ it('assigns submit method to currentDialog and can be called externally', async () => {
1601
+ const engine = new FlowEngine();
1602
+ const flowSettings = new FlowSettings(engine);
1603
+ const TestFlowModel = createIsolatedFlowModel('test-submit-1');
1604
+ const model = new TestFlowModel({ uid: 'm-submit-external', flowEngine: engine });
1605
+
1606
+ TestFlowModel.registerFlow({
1607
+ key: 'submitFlow',
1608
+ steps: {
1609
+ step: {
1610
+ title: 'Step',
1611
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
1612
+ },
1613
+ },
1614
+ });
1615
+
1616
+ const info = vi.fn();
1617
+ const error = vi.fn();
1618
+ const success = vi.fn();
1619
+ model.context.defineProperty('message', { value: { info, error, success } });
1620
+
1621
+ const setStepParams = vi.spyOn(model as any, 'setStepParams');
1622
+ const saveStepParams = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
1623
+
1624
+ let capturedDialog: any;
1625
+ model.context.defineProperty('viewer', {
1626
+ value: {
1627
+ dialog: ({ content }) => {
1628
+ capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
1629
+ if (typeof content === 'function') {
1630
+ content(capturedDialog, { defineMethod: vi.fn() });
1631
+ }
1632
+ return capturedDialog;
1633
+ },
1634
+ },
1635
+ });
1636
+
1637
+ await flowSettings.open({ model, flowKey: 'submitFlow', stepKey: 'step' } as any);
1638
+
1639
+ // Verify that submit method was assigned to dialog
1640
+ expect(typeof capturedDialog.submit).toBe('function');
1641
+
1642
+ // Call submit method externally
1643
+ await capturedDialog.submit();
1644
+
1645
+ // Verify that save flow was executed
1646
+ expect(setStepParams).toHaveBeenCalled();
1647
+ expect(saveStepParams).toHaveBeenCalled();
1648
+ expect(success).toHaveBeenCalledWith('Configuration saved');
1649
+ expect(capturedDialog.close).toHaveBeenCalled();
1650
+ });
1651
+
1652
+ it('submit method handles multiple steps correctly', async () => {
1653
+ const engine = new FlowEngine();
1654
+ const flowSettings = new FlowSettings(engine);
1655
+ const TestFlowModel = createIsolatedFlowModel('test-submit-2');
1656
+ const model = new TestFlowModel({ uid: 'm-submit-multi', flowEngine: engine });
1657
+
1658
+ const beforeHook1 = vi.fn();
1659
+ const afterHook1 = vi.fn();
1660
+ const beforeHook2 = vi.fn();
1661
+ const afterHook2 = vi.fn();
1662
+
1663
+ TestFlowModel.registerFlow({
1664
+ key: 'multiStepFlow',
1665
+ steps: {
1666
+ step1: {
1667
+ title: 'Step 1',
1668
+ beforeParamsSave: beforeHook1,
1669
+ afterParamsSave: afterHook1,
1670
+ uiSchema: { field1: { type: 'string', 'x-component': 'Input' } },
1671
+ },
1672
+ step2: {
1673
+ title: 'Step 2',
1674
+ beforeParamsSave: beforeHook2,
1675
+ afterParamsSave: afterHook2,
1676
+ uiSchema: { field2: { type: 'string', 'x-component': 'Input' } },
1677
+ },
1678
+ },
1679
+ });
1680
+
1681
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1682
+
1683
+ const setStepParams = vi.spyOn(model as any, 'setStepParams');
1684
+ const saveStepParams = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
1685
+
1686
+ let capturedDialog: any;
1687
+ model.context.defineProperty('viewer', {
1688
+ value: {
1689
+ dialog: ({ content }) => {
1690
+ capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
1691
+ if (typeof content === 'function') {
1692
+ content(capturedDialog, { defineMethod: vi.fn() });
1693
+ }
1694
+ return capturedDialog;
1695
+ },
1696
+ },
1697
+ });
1698
+
1699
+ await flowSettings.open({ model, flowKey: 'multiStepFlow' } as any);
1700
+
1701
+ // Call submit method
1702
+ await capturedDialog.submit();
1703
+
1704
+ // Verify both steps were processed
1705
+ expect(setStepParams).toHaveBeenCalledTimes(2);
1706
+ expect(setStepParams).toHaveBeenCalledWith('multiStepFlow', 'step1', expect.any(Object));
1707
+ expect(setStepParams).toHaveBeenCalledWith('multiStepFlow', 'step2', expect.any(Object));
1708
+
1709
+ // Verify hooks were called in correct order
1710
+ expect(beforeHook1).toHaveBeenCalled();
1711
+ expect(beforeHook2).toHaveBeenCalled();
1712
+ expect(saveStepParams).toHaveBeenCalled();
1713
+ expect(afterHook1).toHaveBeenCalled();
1714
+ expect(afterHook2).toHaveBeenCalled();
1715
+
1716
+ expect(capturedDialog.close).toHaveBeenCalled();
1717
+ });
1718
+
1719
+ it('submit method handles FlowExitException by closing dialog without error message', async () => {
1720
+ const { FlowExitException } = await import('../utils/exceptions');
1721
+
1722
+ const engine = new FlowEngine();
1723
+ const flowSettings = new FlowSettings(engine);
1724
+ const TestFlowModel = createIsolatedFlowModel('test-submit-3');
1725
+ const model = new TestFlowModel({ uid: 'm-submit-exit', flowEngine: engine });
1726
+
1727
+ TestFlowModel.registerFlow({
1728
+ key: 'exitFlow',
1729
+ steps: {
1730
+ step: {
1731
+ title: 'Step',
1732
+ beforeParamsSave: () => {
1733
+ throw new FlowExitException('exitFlow', 'm-submit-exit', 'Exit requested');
1734
+ },
1735
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
1736
+ },
1737
+ },
1738
+ });
1739
+
1740
+ const info = vi.fn();
1741
+ const error = vi.fn();
1742
+ const success = vi.fn();
1743
+ model.context.defineProperty('message', { value: { info, error, success } });
1744
+
1745
+ vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
1746
+
1747
+ let capturedDialog: any;
1748
+ model.context.defineProperty('viewer', {
1749
+ value: {
1750
+ dialog: ({ content }) => {
1751
+ capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
1752
+ if (typeof content === 'function') {
1753
+ content(capturedDialog, { defineMethod: vi.fn() });
1754
+ }
1755
+ return capturedDialog;
1756
+ },
1757
+ },
1758
+ });
1759
+
1760
+ await flowSettings.open({ model, flowKey: 'exitFlow', stepKey: 'step' } as any);
1761
+
1762
+ // Call submit method
1763
+ await capturedDialog.submit();
1764
+
1765
+ // Verify FlowExitException handling
1766
+ expect(error).not.toHaveBeenCalled(); // Should not show error message
1767
+ expect(success).not.toHaveBeenCalled(); // Should not show success message
1768
+ expect(capturedDialog.close).toHaveBeenCalled(); // Should close dialog
1769
+ });
1770
+
1771
+ it('submit method handles general errors by showing error message and keeping dialog open', async () => {
1772
+ const engine = new FlowEngine();
1773
+ const flowSettings = new FlowSettings(engine);
1774
+ const TestFlowModel = createIsolatedFlowModel('test-submit-4');
1775
+ const model = new TestFlowModel({ uid: 'm-submit-error', flowEngine: engine });
1776
+
1777
+ TestFlowModel.registerFlow({
1778
+ key: 'errorFlow',
1779
+ steps: {
1780
+ step: {
1781
+ title: 'Step',
1782
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
1783
+ },
1784
+ },
1785
+ });
1786
+
1787
+ const info = vi.fn();
1788
+ const error = vi.fn();
1789
+ const success = vi.fn();
1790
+ model.context.defineProperty('message', { value: { info, error, success } });
1791
+
1792
+ // Mock saveStepParams to throw error
1793
+ vi.spyOn(model as any, 'saveStepParams').mockRejectedValue(new Error('Save failed'));
1794
+
1795
+ let capturedDialog: any;
1796
+ model.context.defineProperty('viewer', {
1797
+ value: {
1798
+ dialog: ({ content }) => {
1799
+ capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
1800
+ if (typeof content === 'function') {
1801
+ content(capturedDialog, { defineMethod: vi.fn() });
1802
+ }
1803
+ return capturedDialog;
1804
+ },
1805
+ },
1806
+ });
1807
+
1808
+ // Mock console.error to avoid log noise
1809
+ const originalConsoleError = console.error;
1810
+ console.error = vi.fn();
1811
+
1812
+ await flowSettings.open({ model, flowKey: 'errorFlow', stepKey: 'step' } as any);
1813
+
1814
+ // Call submit method
1815
+ await capturedDialog.submit();
1816
+
1817
+ // Verify error handling
1818
+ expect(error).toHaveBeenCalledWith('Error saving configuration, please check console');
1819
+ expect(success).not.toHaveBeenCalled();
1820
+ expect(capturedDialog.close).not.toHaveBeenCalled(); // Should keep dialog open
1821
+ expect(console.error).toHaveBeenCalledWith('FlowSettings.open: save error', expect.any(Error));
1822
+
1823
+ // Restore console.error
1824
+ console.error = originalConsoleError;
1825
+ });
1826
+
1827
+ it('submit method calls onSaved callback after successful save', async () => {
1828
+ const engine = new FlowEngine();
1829
+ const flowSettings = new FlowSettings(engine);
1830
+ const TestFlowModel = createIsolatedFlowModel('test-submit-5');
1831
+ const model = new TestFlowModel({ uid: 'm-submit-callback', flowEngine: engine });
1832
+
1833
+ TestFlowModel.registerFlow({
1834
+ key: 'callbackFlow',
1835
+ steps: {
1836
+ step: {
1837
+ title: 'Step',
1838
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
1839
+ },
1840
+ },
1841
+ });
1842
+
1843
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1844
+ vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
1845
+
1846
+ let capturedDialog: any;
1847
+ model.context.defineProperty('viewer', {
1848
+ value: {
1849
+ dialog: ({ content }) => {
1850
+ capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
1851
+ if (typeof content === 'function') {
1852
+ content(capturedDialog, { defineMethod: vi.fn() });
1853
+ }
1854
+ return capturedDialog;
1855
+ },
1856
+ },
1857
+ });
1858
+
1859
+ const onSaved = vi.fn();
1860
+ await flowSettings.open({ model, flowKey: 'callbackFlow', stepKey: 'step', onSaved } as any);
1861
+
1862
+ // Call submit method
1863
+ await capturedDialog.submit();
1864
+
1865
+ // Verify onSaved callback was called
1866
+ expect(onSaved).toHaveBeenCalledTimes(1);
1867
+ expect(capturedDialog.close).toHaveBeenCalled();
1868
+ });
1869
+
1870
+ it('submit method handles onSaved callback errors gracefully', async () => {
1871
+ const engine = new FlowEngine();
1872
+ const flowSettings = new FlowSettings(engine);
1873
+ const TestFlowModel = createIsolatedFlowModel('test-submit-6');
1874
+ const model = new TestFlowModel({ uid: 'm-submit-callback-error', flowEngine: engine });
1875
+
1876
+ TestFlowModel.registerFlow({
1877
+ key: 'callbackErrorFlow',
1878
+ steps: {
1879
+ step: {
1880
+ title: 'Step',
1881
+ uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
1882
+ },
1883
+ },
1884
+ });
1885
+
1886
+ model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
1887
+ vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
1888
+
1889
+ let capturedDialog: any;
1890
+ model.context.defineProperty('viewer', {
1891
+ value: {
1892
+ dialog: ({ content }) => {
1893
+ capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
1894
+ if (typeof content === 'function') {
1895
+ content(capturedDialog, { defineMethod: vi.fn() });
1896
+ }
1897
+ return capturedDialog;
1898
+ },
1899
+ },
1900
+ });
1901
+
1902
+ // Mock console.error to avoid log noise
1903
+ const originalConsoleError = console.error;
1904
+ console.error = vi.fn();
1905
+
1906
+ const onSaved = vi.fn().mockRejectedValue(new Error('Callback error'));
1907
+ await flowSettings.open({ model, flowKey: 'callbackErrorFlow', stepKey: 'step', onSaved } as any);
1908
+
1909
+ // Call submit method
1910
+ await capturedDialog.submit();
1911
+
1912
+ // Verify that main save process completed successfully despite callback error
1913
+ expect(onSaved).toHaveBeenCalledTimes(1);
1914
+ expect(capturedDialog.close).toHaveBeenCalled();
1915
+ expect(console.error).toHaveBeenCalledWith('FlowSettings.open: onSaved callback error', expect.any(Error));
1916
+
1917
+ // Restore console.error
1918
+ console.error = originalConsoleError;
1919
+ });
1920
+ });