@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,1185 @@
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 React from 'react';
11
+ import { act, render, screen, userEvent, waitFor } from '@nocobase/test/client';
12
+ import { vi, beforeEach } from 'vitest';
13
+ import {
14
+ AddSubModelButton,
15
+ FlowEngine,
16
+ FlowEngineProvider,
17
+ FlowModel,
18
+ FlowModelContext,
19
+ type IFlowModelRepository,
20
+ } from '@nocobase/flow-engine';
21
+ import { SubModelItem, mergeSubModelItems, transformItems } from '../AddSubModelButton';
22
+ import { App, ConfigProvider } from 'antd';
23
+
24
+ describe('AddSubModelButton - preset settings open on add', () => {
25
+ test('calls openFlowSettings with preset=true for subModel with preset steps', async () => {
26
+ // Arrange: set up engine and models
27
+ const engine = new FlowEngine();
28
+ engine.flowSettings.forceEnable();
29
+
30
+ class ParentModel extends FlowModel {}
31
+
32
+ const openSpy = vi.fn().mockResolvedValue(true);
33
+ class ChildModel extends FlowModel {
34
+ // Register a flow with a preset step and simple uiSchema
35
+ static registerLocalFlows() {
36
+ this.registerFlow({
37
+ key: 'settings',
38
+ title: 'Settings Example',
39
+ steps: {
40
+ quick: {
41
+ title: 'Quick Setup',
42
+ preset: true,
43
+ uiSchema: {
44
+ field: { type: 'string', title: 'Title', 'x-decorator': 'FormItem', 'x-component': 'Input' },
45
+ },
46
+ },
47
+ },
48
+ });
49
+ }
50
+
51
+ // Override to avoid real UI and capture calls
52
+ async openFlowSettings(options?: { preset?: boolean }) {
53
+ openSpy(options);
54
+ return true;
55
+ }
56
+ }
57
+
58
+ // Register models and create a parent instance
59
+ ChildModel.registerLocalFlows();
60
+ engine.registerModels({ ParentModel, ChildModel });
61
+ const parent = engine.createModel<ParentModel>({ use: 'ParentModel', uid: 'parent' });
62
+
63
+ // Render AddSubModelButton inside providers so LazyDropdown works
64
+ render(
65
+ <FlowEngineProvider engine={engine}>
66
+ <ConfigProvider>
67
+ <App>
68
+ <AddSubModelButton
69
+ model={parent}
70
+ subModelKey="items"
71
+ items={[
72
+ {
73
+ key: 'child',
74
+ label: 'Add Child',
75
+ createModelOptions: { use: 'ChildModel' },
76
+ },
77
+ ]}
78
+ >
79
+ Add SubModel
80
+ </AddSubModelButton>
81
+ </App>
82
+ </ConfigProvider>
83
+ </FlowEngineProvider>,
84
+ );
85
+
86
+ // Act: open dropdown and click the add item
87
+ await act(async () => {
88
+ await userEvent.click(screen.getByText('Add SubModel'));
89
+ });
90
+
91
+ // Ensure menu item appears, then click it
92
+ await waitFor(() => expect(screen.getByText('Add Child')).toBeInTheDocument());
93
+ await act(async () => {
94
+ await userEvent.click(screen.getByText('Add Child'));
95
+ });
96
+
97
+ await waitFor(() => expect(openSpy).toHaveBeenCalled());
98
+ expect(openSpy).toHaveBeenCalledWith(expect.objectContaining({ preset: true }));
99
+ });
100
+ });
101
+
102
+ describe('AddSubModelButton - async group children (nested)', () => {
103
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
104
+
105
+ it('renders group and nested async group leaf items', async () => {
106
+ const engine = new FlowEngine();
107
+ engine.flowSettings.forceEnable();
108
+ class Parent extends FlowModel {}
109
+ engine.registerModels({ Parent });
110
+ const parent = engine.createModel<FlowModel>({ use: 'Parent', uid: 'p1' });
111
+
112
+ const items = async () => [
113
+ {
114
+ key: 'async-group',
115
+ label: 'Async Group',
116
+ type: 'group' as const,
117
+ searchable: true,
118
+ children: async () => {
119
+ await sleep(30);
120
+ return [
121
+ { key: 'g-leaf-1', label: 'G-Leaf-1', createModelOptions: { use: 'Parent' } },
122
+ {
123
+ key: 'nested-group',
124
+ label: 'Nested Group',
125
+ type: 'group' as const,
126
+ children: async () => {
127
+ await sleep(30);
128
+ return [
129
+ { key: 'nested-leaf-1', label: 'Nested-Leaf-1', createModelOptions: { use: 'Parent' } },
130
+ { key: 'nested-leaf-2', label: 'Nested-Leaf-2', createModelOptions: { use: 'Parent' } },
131
+ ];
132
+ },
133
+ },
134
+ ];
135
+ },
136
+ },
137
+ ];
138
+
139
+ render(
140
+ <FlowEngineProvider engine={engine}>
141
+ <ConfigProvider>
142
+ <App>
143
+ <AddSubModelButton model={parent} subModelKey="items" items={items as any}>
144
+ Open Menu
145
+ </AddSubModelButton>
146
+ </App>
147
+ </ConfigProvider>
148
+ </FlowEngineProvider>,
149
+ );
150
+
151
+ await act(async () => {
152
+ await userEvent.click(screen.getByText('Open Menu'));
153
+ });
154
+
155
+ await waitFor(() => expect(screen.getByText('Async Group')).toBeInTheDocument());
156
+ await waitFor(() => expect(screen.getByText('G-Leaf-1')).toBeInTheDocument());
157
+ await waitFor(() => expect(screen.getByText('Nested-Leaf-1')).toBeInTheDocument());
158
+ await waitFor(() => expect(screen.getByText('Nested-Leaf-2')).toBeInTheDocument());
159
+ });
160
+ });
161
+
162
+ describe('transformItems - searchable flags', () => {
163
+ it('preserves searchable + placeholder on non-group submenu items', async () => {
164
+ const engine = new FlowEngine();
165
+ engine.flowSettings.forceEnable();
166
+ class Parent extends FlowModel {}
167
+ engine.registerModels({ Parent });
168
+ const parent = engine.createModel<FlowModel>({ use: 'Parent' });
169
+
170
+ const items = [
171
+ {
172
+ key: 'submenu',
173
+ label: 'Pick',
174
+ searchable: true,
175
+ searchPlaceholder: 'Search blocks',
176
+ children: [
177
+ { key: 'a', label: 'Alpha', createModelOptions: { use: 'Parent' } },
178
+ { key: 'b', label: 'Beta', createModelOptions: { use: 'Parent' } },
179
+ ],
180
+ },
181
+ ];
182
+
183
+ const factory = transformItems(items as any, parent, 'items', 'array');
184
+ const resolved = await (typeof factory === 'function' ? factory() : factory);
185
+ expect(resolved).toHaveLength(1);
186
+ const submenu = resolved[0] as any;
187
+ expect(submenu.searchable).toBe(true);
188
+ expect(submenu.searchPlaceholder).toBe('Search blocks');
189
+ expect(Array.isArray(submenu.children)).toBe(true);
190
+ });
191
+ });
192
+
193
+ describe('transformItems - toggleable items', () => {
194
+ class ToggleParent extends FlowModel {}
195
+ class ToggleChild extends FlowModel {}
196
+
197
+ const setupEngine = () => {
198
+ const engine = new FlowEngine();
199
+ engine.flowSettings.forceEnable();
200
+ engine.registerModels({ ToggleParent, ToggleChild });
201
+ return engine;
202
+ };
203
+
204
+ it('marks toggleable item as active when matching sub model exists', async () => {
205
+ const engine = setupEngine();
206
+ const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-on' });
207
+ const child = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-on' });
208
+ parent.addSubModel('items', child);
209
+
210
+ const definition: SubModelItem[] = [
211
+ {
212
+ key: 'toggle-child',
213
+ label: 'Toggle Child',
214
+ toggleable: true,
215
+ useModel: 'ToggleChild',
216
+ createModelOptions: { use: 'ToggleChild' },
217
+ },
218
+ ];
219
+
220
+ const factory = transformItems(definition, parent, 'items', 'array');
221
+ const resolved = await (typeof factory === 'function' ? factory() : Promise.resolve(factory));
222
+ const toggleItem = resolved[0];
223
+
224
+ expect(toggleItem.isToggled).toBe(true);
225
+ expect(toggleItem.keepDropdownOpen).toBe(true);
226
+ expect(typeof definition[0].customRemove).toBe('function');
227
+
228
+ const { getByRole } = render(<>{toggleItem.label}</>);
229
+ expect(getByRole('switch')).toHaveAttribute('aria-checked', 'true');
230
+
231
+ // customRemove 应当能销毁已存在的子模型
232
+ await definition[0].customRemove?.(parent.context, definition[0]);
233
+ expect(((parent.subModels as any).items || []).length).toBe(0);
234
+ });
235
+
236
+ it('keeps toggleable item off when sub model missing', async () => {
237
+ const engine = setupEngine();
238
+ const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-off' });
239
+
240
+ const definition: SubModelItem[] = [
241
+ {
242
+ key: 'toggle-child',
243
+ label: 'Toggle Child',
244
+ toggleable: true,
245
+ useModel: 'ToggleChild',
246
+ createModelOptions: { use: 'ToggleChild' },
247
+ },
248
+ ];
249
+
250
+ const factory = transformItems(definition, parent, 'items', 'array');
251
+ const resolved = await (typeof factory === 'function' ? factory() : Promise.resolve(factory));
252
+ const toggleItem = resolved[0];
253
+
254
+ expect(toggleItem.isToggled).toBe(false);
255
+ const { getByRole } = render(<>{toggleItem.label}</>);
256
+ expect(getByRole('switch')).toHaveAttribute('aria-checked', 'false');
257
+ });
258
+
259
+ it('respects keepDropdownOpen override on toggleable items', async () => {
260
+ const engine = setupEngine();
261
+ const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-keep' });
262
+
263
+ const definition: SubModelItem[] = [
264
+ {
265
+ key: 'toggle-child-keep',
266
+ label: 'Toggle Child Keep',
267
+ toggleable: true,
268
+ useModel: 'ToggleChild',
269
+ keepDropdownOpen: false,
270
+ createModelOptions: { use: 'ToggleChild' },
271
+ },
272
+ ];
273
+
274
+ const factory = transformItems(definition, parent, 'items', 'array');
275
+ const resolved = await (factory as () => Promise<any[]>)();
276
+ const toggleItem = resolved[0];
277
+
278
+ expect(toggleItem.keepDropdownOpen).toBe(false);
279
+ });
280
+
281
+ it('removes object sub model via default remove handler when toggleDetector provided', async () => {
282
+ const engine = new FlowEngine();
283
+ engine.flowSettings.forceEnable();
284
+
285
+ class ObjectParent extends FlowModel {}
286
+ class ObjectChild extends FlowModel {}
287
+
288
+ engine.registerModels({ ObjectParent, ObjectChild });
289
+ const parent = engine.createModel<ObjectParent>({ use: 'ObjectParent', uid: 'object-parent' });
290
+ const child = engine.createModel<ObjectChild>({ use: 'ObjectChild', uid: 'object-child' });
291
+ parent.setSubModel('single', child);
292
+
293
+ const afterSubModelRemove = vi.fn();
294
+
295
+ render(
296
+ <FlowEngineProvider engine={engine}>
297
+ <ConfigProvider>
298
+ <App>
299
+ <AddSubModelButton
300
+ model={parent}
301
+ subModelKey="single"
302
+ subModelType="object"
303
+ items={[
304
+ {
305
+ key: 'toggle-object',
306
+ label: 'Toggle Object',
307
+ toggleDetector: async (ctx) => Boolean((ctx.model.subModels as any).single),
308
+ createModelOptions: { use: 'ObjectChild' },
309
+ keepDropdownOpen: false,
310
+ },
311
+ ]}
312
+ afterSubModelRemove={afterSubModelRemove}
313
+ >
314
+ Toggle Menu
315
+ </AddSubModelButton>
316
+ </App>
317
+ </ConfigProvider>
318
+ </FlowEngineProvider>,
319
+ );
320
+
321
+ await act(async () => {
322
+ await userEvent.click(screen.getByText('Toggle Menu'));
323
+ });
324
+
325
+ await waitFor(() => expect(screen.getByText('Toggle Object')).toBeInTheDocument());
326
+ expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true');
327
+
328
+ await act(async () => {
329
+ await userEvent.click(screen.getByText('Toggle Object'));
330
+ });
331
+
332
+ await waitFor(() =>
333
+ expect(afterSubModelRemove).toHaveBeenCalledWith(expect.objectContaining({ uid: 'object-child' })),
334
+ );
335
+ await waitFor(() => expect((parent.subModels as any).single).toBeUndefined());
336
+ });
337
+ });
338
+
339
+ describe('mergeSubModelItems', () => {
340
+ it('merges multiple sources with divider insertion', async () => {
341
+ const staticItems: SubModelItem[] = [{ key: 'static', label: 'Static' }];
342
+ const asyncItems = async () => [{ key: 'async', label: 'Async' }];
343
+
344
+ const merged = mergeSubModelItems([staticItems, null, asyncItems], { addDividers: true });
345
+ expect(typeof merged).toBe('function');
346
+
347
+ const ctx = {} as FlowModelContext;
348
+ const result = await (merged as (ctx: FlowModelContext) => Promise<SubModelItem[]>)(ctx);
349
+
350
+ expect(result.map((item) => item.key)).toEqual(['static', 'divider-1', 'async']);
351
+ expect(result[1]).toMatchObject({ type: 'divider' });
352
+ });
353
+ });
354
+
355
+ describe('transformItems - caching behaviour', () => {
356
+ class CacheParent extends FlowModel {}
357
+ class CacheChild extends FlowModel {}
358
+
359
+ const setupEngine = () => {
360
+ const engine = new FlowEngine();
361
+ engine.flowSettings.forceEnable();
362
+ engine.registerModels({ CacheParent, CacheChild });
363
+ const parent = engine.createModel<CacheParent>({ use: 'CacheParent', uid: 'cache-parent' });
364
+ return { engine, parent };
365
+ };
366
+
367
+ it('reuses cached result when no toggleable items exist', async () => {
368
+ const { parent } = setupEngine();
369
+ const definition: SubModelItem[] = [{ key: 'basic', label: 'Basic', createModelOptions: { use: 'CacheChild' } }];
370
+
371
+ const factory = transformItems(definition, parent, 'items', 'array');
372
+ expect(typeof factory).toBe('function');
373
+
374
+ const first = await (factory as () => Promise<any[]>)();
375
+ const second = await (factory as () => Promise<any[]>)();
376
+
377
+ expect(second).toBe(first);
378
+ });
379
+
380
+ it('refreshes toggle state after new sub model is added', async () => {
381
+ const { parent, engine } = setupEngine();
382
+ const createDefinition = (): SubModelItem[] => [
383
+ {
384
+ key: 'toggleable',
385
+ label: 'Toggleable',
386
+ toggleable: true,
387
+ useModel: 'CacheChild',
388
+ createModelOptions: { use: 'CacheChild' },
389
+ },
390
+ ];
391
+
392
+ const firstFactory = transformItems(createDefinition(), parent, 'items', 'array');
393
+ expect(typeof firstFactory).toBe('function');
394
+ const before = await (firstFactory as () => Promise<any[]>)();
395
+ expect(before[0].isToggled).toBe(false);
396
+
397
+ const child = engine.createModel<CacheChild>({ use: 'CacheChild', uid: 'cache-child' });
398
+ parent.addSubModel('items', child);
399
+
400
+ const secondFactory = transformItems(createDefinition(), parent, 'items', 'array');
401
+ expect(typeof secondFactory).toBe('function');
402
+ const after = await (secondFactory as () => Promise<any[]>)();
403
+ expect(after[0].isToggled).toBe(true);
404
+ });
405
+ });
406
+
407
+ describe('AddSubModelButton - refreshTargets linkage', () => {
408
+ it('clicking an item with refreshTargets triggers toggle recomputation on target branch', async () => {
409
+ const engine = new FlowEngine();
410
+ engine.flowSettings.forceEnable();
411
+
412
+ class Parent extends FlowModel {}
413
+ class ToggleModel extends FlowModel {}
414
+
415
+ engine.registerModels({ Parent, ToggleModel });
416
+ const parent = engine.createModel<Parent>({ use: 'Parent', uid: 'p-refreshTargets' });
417
+
418
+ const items = [
419
+ {
420
+ key: 'js',
421
+ label: 'JS Group',
422
+ type: 'group' as const,
423
+ children: [
424
+ {
425
+ key: 'add-toggle',
426
+ label: 'Add Toggle',
427
+ refreshTargets: ['top'],
428
+ createModelOptions: { use: 'ToggleModel' },
429
+ },
430
+ ],
431
+ },
432
+ {
433
+ key: 'top',
434
+ label: 'Top Group',
435
+ type: 'group' as const,
436
+ children: [
437
+ {
438
+ key: 'top-toggle',
439
+ label: 'Top Toggle',
440
+ toggleable: true,
441
+ useModel: 'ToggleModel',
442
+ createModelOptions: { use: 'ToggleModel' },
443
+ },
444
+ ],
445
+ },
446
+ ];
447
+
448
+ render(
449
+ <FlowEngineProvider engine={engine}>
450
+ <ConfigProvider>
451
+ <App>
452
+ <AddSubModelButton model={parent} subModelKey="subs" items={items as any} keepDropdownOpen>
453
+ Open
454
+ </AddSubModelButton>
455
+ </App>
456
+ </ConfigProvider>
457
+ </FlowEngineProvider>,
458
+ );
459
+
460
+ const user = userEvent.setup();
461
+ await act(async () => {
462
+ await user.click(screen.getByText('Open'));
463
+ });
464
+
465
+ await waitFor(() => expect(screen.getByText('JS Group')).toBeInTheDocument());
466
+ await waitFor(() => expect(screen.getByText('Add Toggle')).toBeInTheDocument());
467
+ await act(async () => {
468
+ await user.click(screen.getByText('Add Toggle'));
469
+ });
470
+
471
+ await waitFor(() => expect(screen.getByText('Top Group')).toBeInTheDocument());
472
+ await waitFor(() => expect(screen.getByText('Top Toggle')).toBeInTheDocument());
473
+ await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
474
+ });
475
+ });
476
+
477
+ describe('AddSubModelButton - base class menu groups', () => {
478
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
479
+
480
+ it('renders async children provided by subModelBaseClasses', async () => {
481
+ const engine = new FlowEngine();
482
+ engine.flowSettings.forceEnable();
483
+
484
+ class Parent extends FlowModel {}
485
+ class AsyncLeaf extends FlowModel {}
486
+ class AsyncGroup extends FlowModel {
487
+ static meta = {
488
+ label: 'Async Group',
489
+ children: async () => {
490
+ await sleep(20);
491
+ return [
492
+ {
493
+ key: 'async-leaf',
494
+ label: 'Async Leaf',
495
+ createModelOptions: { use: 'AsyncLeaf' },
496
+ },
497
+ ];
498
+ },
499
+ };
500
+ }
501
+
502
+ engine.registerModels({ Parent, AsyncGroup, AsyncLeaf });
503
+ const parent = engine.createModel<Parent>({ use: 'Parent', uid: 'parent-async-group' });
504
+
505
+ render(
506
+ <FlowEngineProvider engine={engine}>
507
+ <ConfigProvider>
508
+ <App>
509
+ <AddSubModelButton model={parent} subModelKey="items" subModelBaseClasses={['AsyncGroup']}>
510
+ Add Item
511
+ </AddSubModelButton>
512
+ </App>
513
+ </ConfigProvider>
514
+ </FlowEngineProvider>,
515
+ );
516
+
517
+ await act(async () => {
518
+ await userEvent.click(screen.getByText('Add Item'));
519
+ });
520
+
521
+ await waitFor(() => expect(screen.getByText('Async Group')).toBeInTheDocument());
522
+ await waitFor(() => expect(screen.getByText('Async Leaf')).toBeInTheDocument());
523
+ });
524
+
525
+ it('skips base class groups whose children resolve to empty', async () => {
526
+ const engine = new FlowEngine();
527
+ engine.flowSettings.forceEnable();
528
+
529
+ class Parent extends FlowModel {}
530
+ class EmptyLeaf extends FlowModel {}
531
+ class EmptyGroup extends FlowModel {
532
+ static meta = {
533
+ label: 'Empty Group',
534
+ children: async () => {
535
+ await sleep(10);
536
+ return [];
537
+ },
538
+ };
539
+ }
540
+ class NonEmptyGroup extends FlowModel {
541
+ static meta = {
542
+ label: 'Available Group',
543
+ children: async () => [
544
+ {
545
+ key: 'available-leaf',
546
+ label: 'Available Leaf',
547
+ createModelOptions: { use: 'EmptyLeaf' },
548
+ },
549
+ ],
550
+ };
551
+ }
552
+
553
+ engine.registerModels({ Parent, EmptyGroup, NonEmptyGroup, EmptyLeaf });
554
+ const parent = engine.createModel<Parent>({ use: 'Parent', uid: 'parent-empty-group' });
555
+
556
+ render(
557
+ <FlowEngineProvider engine={engine}>
558
+ <ConfigProvider>
559
+ <App>
560
+ <AddSubModelButton model={parent} subModelKey="items" subModelBaseClasses={['EmptyGroup', 'NonEmptyGroup']}>
561
+ Open Menu
562
+ </AddSubModelButton>
563
+ </App>
564
+ </ConfigProvider>
565
+ </FlowEngineProvider>,
566
+ );
567
+
568
+ await act(async () => {
569
+ await userEvent.click(screen.getByText('Open Menu'));
570
+ });
571
+
572
+ await waitFor(() => expect(screen.getByText('Available Group')).toBeInTheDocument());
573
+ await waitFor(() => expect(screen.getByText('Available Leaf')).toBeInTheDocument());
574
+ expect(screen.queryByText('Empty Group')).toBeNull();
575
+ });
576
+
577
+ it('renders submenu base class with children and respects meta.sort', async () => {
578
+ const engine = new FlowEngine();
579
+ engine.flowSettings.forceEnable();
580
+
581
+ class Parent extends FlowModel {}
582
+ class Leaf extends FlowModel {}
583
+ class SubmenuBase extends FlowModel {
584
+ static meta = {
585
+ label: 'JS Field',
586
+ menuType: 'submenu' as const,
587
+ sort: 110,
588
+ children: () => [{ key: 'submenu-leaf', label: 'Submenu Leaf', createModelOptions: { use: 'Leaf' } }],
589
+ };
590
+ }
591
+ class GroupBase extends FlowModel {
592
+ static meta = {
593
+ label: 'Group Base',
594
+ sort: 200,
595
+ children: () => [{ key: 'group-leaf', label: 'Group Leaf', createModelOptions: { use: 'Leaf' } }],
596
+ };
597
+ }
598
+
599
+ engine.registerModels({ Parent, Leaf, SubmenuBase, GroupBase });
600
+ const parent = engine.createModel<Parent>({ use: 'Parent', uid: 'parent-submenu' });
601
+
602
+ render(
603
+ <FlowEngineProvider engine={engine}>
604
+ <ConfigProvider>
605
+ <App>
606
+ <AddSubModelButton
607
+ model={parent}
608
+ subModelKey="items"
609
+ subModelBaseClasses={[
610
+ // reversed on purpose; order should be controlled by meta.sort
611
+ 'GroupBase',
612
+ 'SubmenuBase',
613
+ ]}
614
+ >
615
+ Open Menu
616
+ </AddSubModelButton>
617
+ </App>
618
+ </ConfigProvider>
619
+ </FlowEngineProvider>,
620
+ );
621
+
622
+ await act(async () => {
623
+ await userEvent.click(screen.getByText('Open Menu'));
624
+ });
625
+
626
+ // Both base entries should be visible
627
+ const submenu = await screen.findByText('JS Field');
628
+ const group = await screen.findByText('Group Base');
629
+ expect(submenu).toBeInTheDocument();
630
+ expect(group).toBeInTheDocument();
631
+
632
+ // submenu should appear before group due to sort: 110 < 200
633
+ const pos = submenu.compareDocumentPosition(group);
634
+ expect(pos & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
635
+
636
+ // Hover to open submenu children
637
+ await userEvent.hover(submenu);
638
+ await waitFor(() => expect(screen.getByText('Submenu Leaf')).toBeInTheDocument());
639
+ });
640
+
641
+ it('merges explicit items with base class and grouped sources', async () => {
642
+ const engine = new FlowEngine();
643
+ engine.flowSettings.forceEnable();
644
+
645
+ class Parent extends FlowModel {}
646
+ class BaseChild extends FlowModel {}
647
+ BaseChild.define({ label: 'Base Child' });
648
+
649
+ class GroupBase extends FlowModel {}
650
+ GroupBase.define({
651
+ label: 'Group Base',
652
+ children: () => [
653
+ {
654
+ key: 'group-leaf',
655
+ label: 'Group Leaf',
656
+ createModelOptions: { use: 'Parent' },
657
+ },
658
+ ],
659
+ });
660
+
661
+ engine.registerModels({ Parent, BaseChild, GroupBase });
662
+ const parent = engine.createModel<Parent>({ use: 'Parent', uid: 'parent-merged-sources' });
663
+
664
+ render(
665
+ <FlowEngineProvider engine={engine}>
666
+ <ConfigProvider>
667
+ <App>
668
+ <AddSubModelButton
669
+ model={parent}
670
+ subModelKey="items"
671
+ items={[
672
+ {
673
+ key: 'custom-leaf',
674
+ label: 'Custom Leaf',
675
+ createModelOptions: { use: 'BaseChild' },
676
+ },
677
+ ]}
678
+ subModelBaseClass="BaseChild"
679
+ subModelBaseClasses={['GroupBase']}
680
+ >
681
+ Open Menu
682
+ </AddSubModelButton>
683
+ </App>
684
+ </ConfigProvider>
685
+ </FlowEngineProvider>,
686
+ );
687
+
688
+ await act(async () => {
689
+ await userEvent.click(screen.getByText('Open Menu'));
690
+ });
691
+
692
+ await waitFor(() => expect(screen.getByText('Custom Leaf')).toBeInTheDocument());
693
+ await waitFor(() => expect(screen.getByText('Base Child')).toBeInTheDocument());
694
+ await waitFor(() => expect(screen.getByText('Group Base')).toBeInTheDocument());
695
+ await waitFor(() => expect(screen.getByText('Group Leaf')).toBeInTheDocument());
696
+ });
697
+ });
698
+
699
+ describe('AddSubModelButton - toggle interactions', () => {
700
+ it('removes existing toggleable sub model and triggers callbacks', async () => {
701
+ const engine = new FlowEngine();
702
+ engine.flowSettings.forceEnable();
703
+
704
+ class ToggleParent extends FlowModel {}
705
+ const destroySpy = vi.fn();
706
+ class ToggleChild extends FlowModel {
707
+ async destroy() {
708
+ destroySpy();
709
+ return super.destroy();
710
+ }
711
+ }
712
+
713
+ engine.registerModels({ ToggleParent, ToggleChild });
714
+ const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-remove' });
715
+ const existing = engine.createModel<ToggleChild>({ use: 'ToggleChild', uid: 'toggle-child-existing' });
716
+ parent.addSubModel('items', existing);
717
+
718
+ const afterSubModelRemove = vi.fn();
719
+
720
+ render(
721
+ <FlowEngineProvider engine={engine}>
722
+ <ConfigProvider>
723
+ <App>
724
+ <AddSubModelButton
725
+ model={parent}
726
+ subModelKey="items"
727
+ items={[
728
+ {
729
+ key: 'toggle-child',
730
+ label: 'Toggle Child',
731
+ toggleable: true,
732
+ useModel: 'ToggleChild',
733
+ createModelOptions: { use: 'ToggleChild' },
734
+ },
735
+ ]}
736
+ afterSubModelRemove={afterSubModelRemove}
737
+ >
738
+ Toggle Menu
739
+ </AddSubModelButton>
740
+ </App>
741
+ </ConfigProvider>
742
+ </FlowEngineProvider>,
743
+ );
744
+
745
+ await act(async () => {
746
+ await userEvent.click(screen.getByText('Toggle Menu'));
747
+ });
748
+
749
+ await waitFor(() => expect(screen.getByText('Toggle Child')).toBeInTheDocument());
750
+ const switchControl = screen.getByRole('switch');
751
+ expect(switchControl).toHaveAttribute('aria-checked', 'true');
752
+
753
+ await act(async () => {
754
+ await userEvent.click(screen.getByText('Toggle Child'));
755
+ });
756
+
757
+ await waitFor(() => expect(destroySpy).toHaveBeenCalledTimes(1));
758
+ await waitFor(() =>
759
+ expect(afterSubModelRemove).toHaveBeenCalledWith(expect.objectContaining({ uid: 'toggle-child-existing' })),
760
+ );
761
+ expect((((parent.subModels as any).items as FlowModel[]) || []).length).toBe(0);
762
+ });
763
+
764
+ it('creates toggleable sub model and runs lifecycle callbacks', async () => {
765
+ const engine = new FlowEngine();
766
+ engine.flowSettings.forceEnable();
767
+
768
+ class ToggleParent extends FlowModel {}
769
+ const saveSpy = vi.fn();
770
+ class ToggleChild extends FlowModel {
771
+ async openFlowSettings() {
772
+ return false;
773
+ }
774
+
775
+ async afterAddAsSubModel() {
776
+ return Promise.resolve();
777
+ }
778
+
779
+ async save() {
780
+ saveSpy();
781
+ return Promise.resolve();
782
+ }
783
+ }
784
+
785
+ engine.registerModels({ ToggleParent, ToggleChild });
786
+ const parent = engine.createModel<ToggleParent>({ use: 'ToggleParent', uid: 'toggle-parent-add' });
787
+
788
+ const afterSubModelInit = vi.fn();
789
+ const afterSubModelAdd = vi.fn();
790
+
791
+ render(
792
+ <FlowEngineProvider engine={engine}>
793
+ <ConfigProvider>
794
+ <App>
795
+ <AddSubModelButton
796
+ model={parent}
797
+ subModelKey="items"
798
+ items={[
799
+ {
800
+ key: 'toggle-child',
801
+ label: 'Toggle Child',
802
+ toggleable: true,
803
+ useModel: 'ToggleChild',
804
+ createModelOptions: { use: 'ToggleChild' },
805
+ },
806
+ ]}
807
+ afterSubModelInit={afterSubModelInit}
808
+ afterSubModelAdd={afterSubModelAdd}
809
+ >
810
+ Toggle Menu
811
+ </AddSubModelButton>
812
+ </App>
813
+ </ConfigProvider>
814
+ </FlowEngineProvider>,
815
+ );
816
+
817
+ await act(async () => {
818
+ await userEvent.click(screen.getByText('Toggle Menu'));
819
+ });
820
+
821
+ await waitFor(() => expect(screen.getByText('Toggle Child')).toBeInTheDocument());
822
+ await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false'));
823
+
824
+ await act(async () => {
825
+ await userEvent.click(screen.getByText('Toggle Child'));
826
+ });
827
+
828
+ await waitFor(() => expect(afterSubModelInit).toHaveBeenCalledTimes(1));
829
+ await waitFor(() => expect(afterSubModelAdd).toHaveBeenCalledTimes(1));
830
+ await waitFor(() => expect(saveSpy).toHaveBeenCalledTimes(1));
831
+
832
+ const subModels = ((parent.subModels as any).items as FlowModel[]) || [];
833
+ expect(subModels).toHaveLength(1);
834
+ });
835
+ });
836
+
837
+ // ========================
838
+ // Toggleable dropdown UX
839
+ // ========================
840
+ describe('AddSubModelButton toggleable behavior', () => {
841
+ // A simple Toggle model used for toggleable menu items
842
+ class ToggleModel extends FlowModel {}
843
+
844
+ // Minimal fake repository for save/destroy
845
+ class FakeRepo implements IFlowModelRepository<any> {
846
+ findOne = vi.fn().mockResolvedValue(null);
847
+ save = vi.fn().mockResolvedValue({});
848
+ destroy = vi.fn().mockResolvedValue(true);
849
+ move = vi.fn().mockResolvedValue(undefined);
850
+ }
851
+
852
+ function setup() {
853
+ const engine = new FlowEngine();
854
+ engine.flowSettings.forceEnable();
855
+ engine.registerModels({ ToggleModel });
856
+ engine.setModelRepository(new FakeRepo());
857
+
858
+ // Avoid opening any settings UI; proceed directly to add
859
+ vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
860
+
861
+ const parent = engine.createModel<FlowModel>({ use: FlowModel });
862
+
863
+ // static array wrapped in a function to avoid caching pitfalls
864
+ const items = async () => [
865
+ {
866
+ key: 'toggle',
867
+ label: 'Toggle Feature',
868
+ toggleable: true,
869
+ useModel: 'ToggleModel',
870
+ createModelOptions: { use: 'ToggleModel' },
871
+ },
872
+ {
873
+ key: 'group',
874
+ label: 'Async Group',
875
+ type: 'group' as const,
876
+ // async children to simulate heavy branch; should not be cleared on toggle refresh
877
+ children: async () => {
878
+ await new Promise((resolve) => setTimeout(resolve, 10));
879
+ return [
880
+ { key: 'child-a', label: 'Child A' },
881
+ { key: 'child-b', label: 'Child B' },
882
+ ];
883
+ },
884
+ },
885
+ ];
886
+
887
+ const ui = (
888
+ <FlowEngineProvider engine={engine}>
889
+ <ConfigProvider>
890
+ <App>
891
+ <AddSubModelButton model={parent} items={items as any} subModelType="array" subModelKey="subs">
892
+ Open
893
+ </AddSubModelButton>
894
+ </App>
895
+ </ConfigProvider>
896
+ </FlowEngineProvider>
897
+ );
898
+
899
+ return { engine, parent, ui };
900
+ }
901
+
902
+ beforeEach(() => {
903
+ vi.useRealTimers();
904
+ });
905
+
906
+ test('keeps dropdown open and preserves loaded children on toggle add/remove', async () => {
907
+ const { engine, ui } = setup();
908
+ const user = userEvent.setup();
909
+
910
+ render(ui);
911
+
912
+ // open menu
913
+ await user.click(screen.getByText('Open'));
914
+
915
+ // wait async group children to load
916
+ await waitFor(() => expect(screen.getByText('Child A')).toBeInTheDocument());
917
+ expect(screen.getByText('Child B')).toBeInTheDocument();
918
+
919
+ const repo = engine.modelRepository as FakeRepo;
920
+
921
+ // toggle ON (add model)
922
+ await user.click(screen.getByText('Toggle Feature'));
923
+
924
+ // Wait for save to be called after adding
925
+ await waitFor(
926
+ () => {
927
+ expect(repo.save).toHaveBeenCalled();
928
+ },
929
+ { timeout: 3000 },
930
+ );
931
+
932
+ // dropdown should remain open and children should still be visible (no flicker / reload)
933
+ expect(screen.getByText('Async Group')).toBeInTheDocument();
934
+ expect(screen.getByText('Child A')).toBeInTheDocument();
935
+ expect(screen.getByText('Child B')).toBeInTheDocument();
936
+
937
+ // toggle OFF (remove model)
938
+ await user.click(screen.getByText('Toggle Feature'));
939
+
940
+ // still open and children preserved
941
+ expect(screen.getByText('Async Group')).toBeInTheDocument();
942
+ expect(screen.getByText('Child A')).toBeInTheDocument();
943
+ expect(screen.getByText('Child B')).toBeInTheDocument();
944
+
945
+ // ensure destroy was called once for removal with increased timeout
946
+ await waitFor(
947
+ () => {
948
+ expect(repo.destroy).toHaveBeenCalledTimes(1);
949
+ },
950
+ { timeout: 5000 },
951
+ );
952
+ });
953
+
954
+ test('toggle state updates without menu closing', async () => {
955
+ const { ui } = setup();
956
+ const user = userEvent.setup();
957
+
958
+ render(ui);
959
+
960
+ await user.click(screen.getByText('Open'));
961
+ await waitFor(() => expect(screen.getByText('Toggle Feature')).toBeInTheDocument());
962
+
963
+ // add => the switch label should change to toggled state on next paint
964
+ await user.click(screen.getByText('Toggle Feature'));
965
+
966
+ // menu remains visible
967
+ expect(screen.getByText('Toggle Feature')).toBeInTheDocument();
968
+ expect(screen.getByText('Async Group')).toBeInTheDocument();
969
+ });
970
+
971
+ test('nested submenu (static items) toggle keeps menu open and reflects state', async () => {
972
+ const engine = new FlowEngine();
973
+ engine.flowSettings.forceEnable();
974
+ engine.registerModels({ ToggleModel });
975
+ const parent = engine.createModel<FlowModel>({ use: FlowModel });
976
+
977
+ const items = [
978
+ {
979
+ key: 'fields',
980
+ label: 'Fields',
981
+ searchable: true,
982
+ // 非 group 子菜单(需要鼠标悬浮展开)
983
+ children: [
984
+ {
985
+ key: 'f1',
986
+ label: 'Field 1',
987
+ // 基于 stepParams 的二级切换判定
988
+ toggleable: (m: any) => m.getStepParams('fieldSettings', 'init')?.fieldPath === 'f1',
989
+ useModel: 'ToggleModel',
990
+ createModelOptions: {
991
+ use: 'ToggleModel',
992
+ stepParams: {
993
+ fieldSettings: {
994
+ init: { fieldPath: 'f1' },
995
+ },
996
+ },
997
+ },
998
+ },
999
+ ],
1000
+ },
1001
+ ];
1002
+
1003
+ const user = userEvent.setup();
1004
+ render(
1005
+ <FlowEngineProvider engine={engine}>
1006
+ <ConfigProvider>
1007
+ <App>
1008
+ <AddSubModelButton model={parent} items={items as any} subModelKey="subs">
1009
+ Open
1010
+ </AddSubModelButton>
1011
+ </App>
1012
+ </ConfigProvider>
1013
+ </FlowEngineProvider>,
1014
+ );
1015
+
1016
+ // 打开主菜单
1017
+ await user.click(screen.getByText('Open'));
1018
+ // 等待父级子菜单项渲染出来再悬浮展开
1019
+ await waitFor(() => expect(screen.getByText('Fields')).toBeInTheDocument());
1020
+ await user.hover(screen.getByText('Fields'));
1021
+ // 初始未选中
1022
+ await waitFor(() => expect(screen.getByText('Field 1')).toBeInTheDocument());
1023
+ const getSwitch = () => screen.getAllByRole('switch')[0];
1024
+ expect(getSwitch()).toHaveAttribute('aria-checked', 'false');
1025
+
1026
+ // 点击以选中,菜单不应被关闭;仍可见 Fields 和 Field 1
1027
+ // 需要等待子菜单项动画结束,避免 pointer-events: none 导致点击失败
1028
+ const el = screen.getByText('Field 1');
1029
+ await waitFor(() => expect(window.getComputedStyle(el).pointerEvents).not.toBe('none'));
1030
+ await user.click(screen.getByText('Field 1'));
1031
+ await waitFor(() => {
1032
+ expect(screen.getByText('Fields')).toBeInTheDocument();
1033
+ expect(screen.getByText('Field 1')).toBeInTheDocument();
1034
+ expect(getSwitch()).toHaveAttribute('aria-checked', 'true');
1035
+ });
1036
+
1037
+ // 再次点击以取消选中,菜单仍保持
1038
+ // 二级子菜单在点击后可能被 rc-menu 收起,需要先重新展开
1039
+ await user.hover(screen.getByText('Fields'));
1040
+ await waitFor(() => expect(screen.getByText('Field 1')).toBeInTheDocument());
1041
+ {
1042
+ const el = screen.getByText('Field 1');
1043
+ await waitFor(() => expect(window.getComputedStyle(el).pointerEvents).not.toBe('none'));
1044
+ }
1045
+ await user.click(screen.getByText('Field 1'));
1046
+ await waitFor(() => {
1047
+ expect(screen.getByText('Fields')).toBeInTheDocument();
1048
+ expect(screen.getByText('Field 1')).toBeInTheDocument();
1049
+ expect(getSwitch()).toHaveAttribute('aria-checked', 'false');
1050
+ });
1051
+ });
1052
+
1053
+ test('submenu (second-level) toggleable stays open and updates state', async () => {
1054
+ const engine = new FlowEngine();
1055
+ engine.flowSettings.forceEnable();
1056
+ engine.registerModels({ ToggleModel });
1057
+ engine.setModelRepository(new FakeRepo());
1058
+ vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
1059
+
1060
+ const parent = engine.createModel<FlowModel>({ use: FlowModel });
1061
+
1062
+ const items = [
1063
+ {
1064
+ key: 'submenu',
1065
+ label: 'Fields',
1066
+ // create a real submenu (non-group) with static children
1067
+ children: [
1068
+ {
1069
+ key: 'leaf-toggle',
1070
+ label: 'Leaf Toggle',
1071
+ toggleable: true,
1072
+ useModel: 'ToggleModel',
1073
+ createModelOptions: { use: 'ToggleModel' },
1074
+ },
1075
+ ],
1076
+ },
1077
+ ];
1078
+
1079
+ render(
1080
+ <FlowEngineProvider engine={engine}>
1081
+ <ConfigProvider>
1082
+ <App>
1083
+ <AddSubModelButton model={parent} items={items as any} subModelType="array" subModelKey="subs">
1084
+ Open
1085
+ </AddSubModelButton>
1086
+ </App>
1087
+ </ConfigProvider>
1088
+ </FlowEngineProvider>,
1089
+ );
1090
+
1091
+ const user = userEvent.setup();
1092
+ await user.click(screen.getByText('Open'));
1093
+
1094
+ // hover to open submenu
1095
+ // Using click on parent also works in test environment to render children
1096
+ await waitFor(() => expect(screen.getByText('Fields')).toBeInTheDocument());
1097
+ await user.hover(screen.getByText('Fields'));
1098
+ await waitFor(() => expect(screen.getByText('Leaf Toggle')).toBeInTheDocument());
1099
+
1100
+ // click leaf toggle to add
1101
+ await user.click(screen.getByText('Leaf Toggle'));
1102
+
1103
+ // menu should remain visible; submenu parent still visible
1104
+ expect(screen.getByText('Fields')).toBeInTheDocument();
1105
+
1106
+ // 由于点击叶子项后二级子菜单可能被收起,这里先重新展开再断言开关状态
1107
+ await user.hover(screen.getByText('Fields'));
1108
+ await waitFor(() => expect(screen.getByText('Leaf Toggle')).toBeInTheDocument());
1109
+ await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
1110
+ });
1111
+
1112
+ test('top-level toggle updates after opening a second-level branch', async () => {
1113
+ const engine = new FlowEngine();
1114
+ engine.flowSettings.forceEnable();
1115
+ engine.registerModels({ ToggleModel });
1116
+ engine.setModelRepository(new FakeRepo());
1117
+ vi.spyOn(engine.flowSettings, 'open').mockResolvedValue(false as any);
1118
+
1119
+ const parent = engine.createModel<FlowModel>({ use: FlowModel });
1120
+
1121
+ const items = async () => [
1122
+ {
1123
+ key: 'fields',
1124
+ label: 'Fields',
1125
+ type: 'group' as const,
1126
+ // static children (simulate TableColumnModel fields) – must still update after refresh
1127
+ children: [
1128
+ {
1129
+ key: 'id',
1130
+ label: 'ID',
1131
+ toggleable: true,
1132
+ useModel: 'ToggleModel',
1133
+ createModelOptions: { use: 'ToggleModel' },
1134
+ },
1135
+ ],
1136
+ },
1137
+ {
1138
+ key: 'associations',
1139
+ label: 'Associations',
1140
+ type: 'group' as const,
1141
+ // async group to create a loadedChildren entry when opened
1142
+ children: async () => [
1143
+ {
1144
+ key: 'assoc-a',
1145
+ label: 'Assoc A',
1146
+ type: 'group' as const,
1147
+ children: [
1148
+ { key: 'child-a', label: 'Child A' },
1149
+ { key: 'child-b', label: 'Child B' },
1150
+ ],
1151
+ },
1152
+ ],
1153
+ },
1154
+ ];
1155
+
1156
+ render(
1157
+ <FlowEngineProvider engine={engine}>
1158
+ <ConfigProvider>
1159
+ <App>
1160
+ <AddSubModelButton model={parent} items={items as any} subModelType="array" subModelKey="subs">
1161
+ Open
1162
+ </AddSubModelButton>
1163
+ </App>
1164
+ </ConfigProvider>
1165
+ </FlowEngineProvider>,
1166
+ );
1167
+
1168
+ const user = userEvent.setup();
1169
+ await user.click(screen.getByText('Open'));
1170
+
1171
+ // open a second-level branch (to simulate previously opened submenu)
1172
+ await waitFor(() => expect(screen.getByText('Associations')).toBeInTheDocument());
1173
+ await user.hover(screen.getByText('Associations'));
1174
+ await waitFor(() => expect(screen.getByText('Assoc A')).toBeInTheDocument());
1175
+
1176
+ // click top-level item 'ID'
1177
+ await user.click(screen.getByText('ID'));
1178
+
1179
+ // menu stays open and the switch for ID should be ON
1180
+ await waitFor(() => expect(screen.getByText('Fields')).toBeInTheDocument());
1181
+ await waitFor(() => expect(screen.getByText('ID')).toBeInTheDocument());
1182
+ // there is only one switch in this minimal case; ensure it is ON
1183
+ await waitFor(() => expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'));
1184
+ });
1185
+ });