@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/BlockScopedFlowEngine.js +0 -1
  4. package/lib/FlowDefinition.d.ts +6 -0
  5. package/lib/FlowSchemaRegistry.d.ts +154 -0
  6. package/lib/FlowSchemaRegistry.js +1427 -0
  7. package/lib/JSRunner.d.ts +15 -0
  8. package/lib/JSRunner.js +82 -7
  9. package/lib/ViewScopedFlowEngine.js +8 -1
  10. package/lib/acl/Acl.js +13 -3
  11. package/lib/components/FlowContextSelector.js +155 -10
  12. package/lib/components/MobilePopup.js +6 -5
  13. package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
  14. package/lib/components/dnd/gridDragPlanner.js +59 -3
  15. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  17. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  18. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +21 -3
  19. package/lib/components/subModel/AddSubModelButton.js +27 -1
  20. package/lib/components/subModel/utils.js +2 -2
  21. package/lib/components/variables/VariableInput.js +9 -4
  22. package/lib/components/variables/VariableTag.js +46 -39
  23. package/lib/components/variables/utils.d.ts +7 -0
  24. package/lib/components/variables/utils.js +42 -2
  25. package/lib/data-source/index.d.ts +7 -27
  26. package/lib/data-source/index.js +84 -51
  27. package/lib/executor/FlowExecutor.d.ts +2 -1
  28. package/lib/executor/FlowExecutor.js +190 -26
  29. package/lib/flow-schema-registry/fieldBinding.d.ts +32 -0
  30. package/lib/flow-schema-registry/fieldBinding.js +165 -0
  31. package/lib/flow-schema-registry/modelPatches.d.ts +16 -0
  32. package/lib/flow-schema-registry/modelPatches.js +235 -0
  33. package/lib/flow-schema-registry/schemaInference.d.ts +17 -0
  34. package/lib/flow-schema-registry/schemaInference.js +207 -0
  35. package/lib/flow-schema-registry/utils.d.ts +25 -0
  36. package/lib/flow-schema-registry/utils.js +293 -0
  37. package/lib/flowContext.d.ts +230 -7
  38. package/lib/flowContext.js +2270 -148
  39. package/lib/flowEngine.d.ts +160 -1
  40. package/lib/flowEngine.js +387 -27
  41. package/lib/flowI18n.js +6 -4
  42. package/lib/flowSettings.d.ts +14 -6
  43. package/lib/flowSettings.js +51 -17
  44. package/lib/index.d.ts +8 -1
  45. package/lib/index.js +24 -1
  46. package/lib/lazy-helper.d.ts +14 -0
  47. package/lib/lazy-helper.js +71 -0
  48. package/lib/locale/en-US.json +9 -2
  49. package/lib/locale/index.d.ts +14 -0
  50. package/lib/locale/zh-CN.json +8 -1
  51. package/lib/models/CollectionFieldModel.d.ts +1 -0
  52. package/lib/models/CollectionFieldModel.js +3 -2
  53. package/lib/models/DisplayItemModel.d.ts +1 -1
  54. package/lib/models/EditableItemModel.d.ts +1 -1
  55. package/lib/models/FilterableItemModel.d.ts +1 -1
  56. package/lib/models/flowModel.d.ts +7 -0
  57. package/lib/models/flowModel.js +83 -8
  58. package/lib/provider.js +7 -6
  59. package/lib/resources/baseRecordResource.d.ts +5 -0
  60. package/lib/resources/baseRecordResource.js +24 -0
  61. package/lib/resources/multiRecordResource.d.ts +1 -0
  62. package/lib/resources/multiRecordResource.js +11 -4
  63. package/lib/resources/singleRecordResource.js +2 -0
  64. package/lib/resources/sqlResource.d.ts +4 -3
  65. package/lib/resources/sqlResource.js +8 -3
  66. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  67. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  68. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  69. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  70. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  71. package/lib/runjs-context/contexts/base.js +706 -41
  72. package/lib/runjs-context/contributions.d.ts +33 -0
  73. package/lib/runjs-context/contributions.js +88 -0
  74. package/lib/runjs-context/helpers.js +12 -1
  75. package/lib/runjs-context/registry.d.ts +1 -1
  76. package/lib/runjs-context/setup.js +23 -9
  77. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  78. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  79. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  80. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  81. package/lib/runjs-context/snippets/index.d.ts +11 -1
  82. package/lib/runjs-context/snippets/index.js +61 -40
  83. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  84. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  85. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  86. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  87. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  88. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  89. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  90. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  91. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  92. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  93. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  94. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  95. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  96. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  97. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  98. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  99. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  100. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  101. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  102. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  103. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  104. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  105. package/lib/runjsLibs.d.ts +28 -0
  106. package/lib/runjsLibs.js +532 -0
  107. package/lib/scheduler/ModelOperationScheduler.d.ts +7 -1
  108. package/lib/scheduler/ModelOperationScheduler.js +28 -23
  109. package/lib/server.d.ts +10 -0
  110. package/lib/server.js +32 -0
  111. package/lib/types.d.ts +296 -1
  112. package/lib/utils/associationObjectVariable.d.ts +2 -2
  113. package/lib/utils/createCollectionContextMeta.js +1 -0
  114. package/lib/utils/createEphemeralContext.js +2 -2
  115. package/lib/utils/dateVariable.d.ts +16 -0
  116. package/lib/utils/dateVariable.js +380 -0
  117. package/lib/utils/exceptions.d.ts +7 -0
  118. package/lib/utils/exceptions.js +10 -0
  119. package/lib/utils/index.d.ts +8 -3
  120. package/lib/utils/index.js +49 -0
  121. package/lib/utils/params-resolvers.js +16 -9
  122. package/lib/utils/parsePathnameToViewParams.js +1 -1
  123. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  124. package/lib/utils/resolveModuleUrl.js +65 -0
  125. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  126. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  127. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  128. package/lib/utils/runjsModuleLoader.js +422 -0
  129. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  130. package/lib/utils/runjsTemplateCompat.js +743 -0
  131. package/lib/utils/runjsValue.d.ts +29 -0
  132. package/lib/utils/runjsValue.js +275 -0
  133. package/lib/utils/safeGlobals.d.ts +18 -8
  134. package/lib/utils/safeGlobals.js +164 -17
  135. package/lib/utils/schema-utils.d.ts +17 -1
  136. package/lib/utils/schema-utils.js +80 -0
  137. package/lib/views/FlowView.d.ts +7 -1
  138. package/lib/views/createViewMeta.d.ts +0 -7
  139. package/lib/views/createViewMeta.js +19 -70
  140. package/lib/views/index.d.ts +1 -2
  141. package/lib/views/index.js +4 -3
  142. package/lib/views/runViewBeforeClose.d.ts +10 -0
  143. package/lib/views/runViewBeforeClose.js +45 -0
  144. package/lib/views/useDialog.d.ts +2 -1
  145. package/lib/views/useDialog.js +28 -6
  146. package/lib/views/useDrawer.d.ts +2 -1
  147. package/lib/views/useDrawer.js +27 -5
  148. package/lib/views/usePage.d.ts +6 -1
  149. package/lib/views/usePage.js +53 -9
  150. package/lib/views/usePopover.js +4 -1
  151. package/lib/views/viewEvents.d.ts +17 -0
  152. package/lib/views/viewEvents.js +90 -0
  153. package/package.json +5 -5
  154. package/server.d.ts +1 -0
  155. package/server.js +1 -0
  156. package/src/BlockScopedFlowEngine.ts +2 -5
  157. package/src/FlowSchemaRegistry.ts +1799 -0
  158. package/src/JSRunner.ts +111 -5
  159. package/src/ViewScopedFlowEngine.ts +8 -0
  160. package/src/__tests__/FlowSchemaRegistry.test.ts +1951 -0
  161. package/src/__tests__/JSRunner.test.ts +91 -1
  162. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  163. package/src/__tests__/flow-engine.test.ts +48 -0
  164. package/src/__tests__/flowContext.test.ts +693 -1
  165. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  166. package/src/__tests__/flowEngine.modelLoaders.test.ts +249 -0
  167. package/src/__tests__/flowEngine.saveModel.test.ts +4 -0
  168. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  169. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  170. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  171. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  172. package/src/__tests__/flowSettings.test.ts +94 -15
  173. package/src/__tests__/provider.test.tsx +0 -5
  174. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  175. package/src/__tests__/runjsContext.test.ts +26 -7
  176. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  177. package/src/__tests__/runjsContextRuntime.test.ts +5 -3
  178. package/src/__tests__/runjsContributions.test.ts +89 -0
  179. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  180. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  181. package/src/__tests__/runjsLocales.test.ts +4 -1
  182. package/src/__tests__/runjsPreprocessDefault.test.ts +72 -0
  183. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  184. package/src/__tests__/runjsSnippets.test.ts +40 -3
  185. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  186. package/src/acl/Acl.tsx +3 -3
  187. package/src/components/FlowContextSelector.tsx +208 -12
  188. package/src/components/MobilePopup.tsx +4 -2
  189. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
  190. package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
  191. package/src/components/dnd/gridDragPlanner.ts +68 -2
  192. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  193. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  194. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  195. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  196. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +31 -4
  197. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  198. package/src/components/subModel/AddSubModelButton.tsx +32 -2
  199. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +143 -32
  200. package/src/components/subModel/utils.ts +1 -1
  201. package/src/components/variables/VariableInput.tsx +12 -4
  202. package/src/components/variables/VariableTag.tsx +54 -45
  203. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  204. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  205. package/src/components/variables/__tests__/utils.test.ts +81 -3
  206. package/src/components/variables/utils.ts +67 -6
  207. package/src/data-source/index.ts +88 -110
  208. package/src/executor/FlowExecutor.ts +230 -28
  209. package/src/executor/__tests__/flowExecutor.test.ts +123 -0
  210. package/src/flow-schema-registry/fieldBinding.ts +171 -0
  211. package/src/flow-schema-registry/modelPatches.ts +260 -0
  212. package/src/flow-schema-registry/schemaInference.ts +210 -0
  213. package/src/flow-schema-registry/utils.ts +268 -0
  214. package/src/flowContext.ts +2989 -212
  215. package/src/flowEngine.ts +434 -23
  216. package/src/flowI18n.ts +7 -5
  217. package/src/flowSettings.ts +58 -18
  218. package/src/index.ts +15 -1
  219. package/src/lazy-helper.tsx +57 -0
  220. package/src/locale/en-US.json +9 -2
  221. package/src/locale/zh-CN.json +8 -1
  222. package/src/models/CollectionFieldModel.tsx +3 -1
  223. package/src/models/DisplayItemModel.tsx +1 -1
  224. package/src/models/EditableItemModel.tsx +1 -1
  225. package/src/models/FilterableItemModel.tsx +1 -1
  226. package/src/models/__tests__/dispatchEvent.when.test.ts +768 -0
  227. package/src/models/__tests__/flowModel.clone.test.ts +416 -0
  228. package/src/models/__tests__/flowModel.test.ts +20 -4
  229. package/src/models/flowModel.tsx +112 -7
  230. package/src/provider.tsx +9 -7
  231. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  232. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  233. package/src/resources/baseRecordResource.ts +31 -0
  234. package/src/resources/multiRecordResource.ts +11 -4
  235. package/src/resources/singleRecordResource.ts +3 -0
  236. package/src/resources/sqlResource.ts +11 -6
  237. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  238. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  239. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  240. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  241. package/src/runjs-context/contexts/base.ts +715 -44
  242. package/src/runjs-context/contributions.ts +88 -0
  243. package/src/runjs-context/helpers.ts +11 -1
  244. package/src/runjs-context/registry.ts +1 -1
  245. package/src/runjs-context/setup.ts +25 -9
  246. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  247. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  248. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  249. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  250. package/src/runjs-context/snippets/index.ts +75 -41
  251. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  252. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  253. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  254. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  255. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  256. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  257. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  258. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  259. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  260. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  261. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  262. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  263. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  264. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  265. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  266. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  267. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  268. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  269. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  270. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  271. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  272. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  273. package/src/runjsLibs.ts +622 -0
  274. package/src/scheduler/ModelOperationScheduler.ts +41 -24
  275. package/src/server.ts +11 -0
  276. package/src/types.ts +359 -1
  277. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  278. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  279. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  280. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  281. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  282. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  283. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  284. package/src/utils/__tests__/utils.test.ts +157 -0
  285. package/src/utils/associationObjectVariable.ts +2 -2
  286. package/src/utils/createCollectionContextMeta.ts +1 -0
  287. package/src/utils/createEphemeralContext.ts +5 -4
  288. package/src/utils/dateVariable.ts +397 -0
  289. package/src/utils/exceptions.ts +11 -0
  290. package/src/utils/index.ts +38 -3
  291. package/src/utils/params-resolvers.ts +23 -9
  292. package/src/utils/parsePathnameToViewParams.ts +2 -2
  293. package/src/utils/resolveModuleUrl.ts +91 -0
  294. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  295. package/src/utils/runjsModuleLoader.ts +553 -0
  296. package/src/utils/runjsTemplateCompat.ts +828 -0
  297. package/src/utils/runjsValue.ts +287 -0
  298. package/src/utils/safeGlobals.ts +188 -17
  299. package/src/utils/schema-utils.ts +109 -1
  300. package/src/views/FlowView.tsx +11 -1
  301. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  302. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  303. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
  304. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  305. package/src/views/createViewMeta.ts +22 -75
  306. package/src/views/index.tsx +1 -2
  307. package/src/views/runViewBeforeClose.ts +19 -0
  308. package/src/views/useDialog.tsx +34 -5
  309. package/src/views/useDrawer.tsx +33 -4
  310. package/src/views/usePage.tsx +63 -8
  311. package/src/views/usePopover.tsx +4 -1
  312. package/src/views/viewEvents.ts +55 -0
@@ -8,12 +8,13 @@
8
8
  */
9
9
 
10
10
  import React, { useCallback, useRef, useMemo, useState, useEffect } from 'react';
11
- import { Button, Cascader, Tooltip } from 'antd';
11
+ import { Button, Cascader, Input, Tooltip, theme } from 'antd';
12
12
  import { QuestionCircleOutlined } from '@ant-design/icons';
13
13
  import { cx, css } from '@emotion/css';
14
14
  import type { ContextSelectorItem, FlowContextSelectorProps } from './variables/types';
15
15
  import {
16
16
  buildContextSelectorItems,
17
+ filterLoadedContextSelectorItems,
17
18
  formatPathToValue,
18
19
  parseValueToPath,
19
20
  preloadContextSelectorPath,
@@ -34,6 +35,52 @@ const cascaderPopupAutoHeightClassName = css`
34
35
  }
35
36
  `;
36
37
 
38
+ type SelectedPathInfo = {
39
+ text: string;
40
+ meta?: ContextSelectorItem['meta'];
41
+ };
42
+
43
+ const normalizePath = (path: unknown): string[] | undefined => {
44
+ if (!Array.isArray(path)) {
45
+ return undefined;
46
+ }
47
+
48
+ return path.map((segment) => String(segment));
49
+ };
50
+
51
+ const getSelectedPathInfo = (path: string[] | undefined, options: ContextSelectorItem[]): SelectedPathInfo => {
52
+ if (!Array.isArray(path) || path.length === 0) {
53
+ return { text: '', meta: undefined };
54
+ }
55
+
56
+ const labels: string[] = [];
57
+ let currentOptions = options;
58
+ let selectedMeta: ContextSelectorItem['meta'] | undefined;
59
+
60
+ for (const segment of path) {
61
+ const matchedOption = currentOptions.find((item) => String(item.value) === String(segment));
62
+ if (!matchedOption) {
63
+ break;
64
+ }
65
+
66
+ const label =
67
+ typeof matchedOption.meta?.title === 'string'
68
+ ? matchedOption.meta.title
69
+ : typeof matchedOption.label === 'string'
70
+ ? matchedOption.label
71
+ : String(matchedOption.value);
72
+
73
+ labels.push(label);
74
+ selectedMeta = matchedOption.meta;
75
+ currentOptions = Array.isArray(matchedOption.children) ? matchedOption.children : [];
76
+ }
77
+
78
+ return {
79
+ text: labels.join(' / '),
80
+ meta: selectedMeta,
81
+ };
82
+ };
83
+
37
84
  const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
38
85
  value,
39
86
  onChange,
@@ -47,13 +94,15 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
47
94
  ignoreFieldNames,
48
95
  ...cascaderProps
49
96
  }) => {
97
+ const { token } = theme.useToken();
98
+
50
99
  // 记录最后点击的路径,用于双击检测
51
100
  const lastSelectedRef = useRef<{ path: string; time: number } | null>(null);
52
101
 
53
102
  const { resolvedMetaTree, loading } = useResolvedMetaTree(metaTree);
54
103
 
55
104
  // 获取引擎上下文中的翻译函数,若不可用则回退为原文
56
- const flowCtx = useFlowContext<any>();
105
+ const flowCtx = useFlowContext();
57
106
 
58
107
  const translateOptions = useCallback(
59
108
  (items: ContextSelectorItem[] | undefined): ContextSelectorItem[] => {
@@ -63,7 +112,9 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
63
112
  const meta = o.meta;
64
113
  const disabled = meta ? !!(typeof meta.disabled === 'function' ? meta.disabled() : meta.disabled) : false;
65
114
  const disabledReason = meta
66
- ? ((typeof meta.disabledReason === 'function' ? meta.disabledReason() : meta.disabledReason) as any)
115
+ ? typeof meta.disabledReason === 'function'
116
+ ? meta.disabledReason()
117
+ : meta.disabledReason
67
118
  : undefined;
68
119
 
69
120
  // 文本国际化:仅当 label 为字符串时进行翻译
@@ -98,7 +149,11 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
98
149
 
99
150
  // 用于强制重新渲染的状态
100
151
  const [updateFlag, setUpdateFlag] = useState(0);
152
+ const [searchText, setSearchText] = useState('');
153
+ const [dropdownOpen, setDropdownOpen] = useState(false);
154
+ const inlineFocusByPointerRef = useRef(false);
101
155
  const triggerUpdate = useCallback(() => setUpdateFlag((prev) => prev + 1), []);
156
+ const isSearchEnabled = showSearch || children === null;
102
157
 
103
158
  // 构建选项
104
159
  // 注意:rc-cascader 内部对 options 做了基于引用的缓存(useEntities)。
@@ -106,13 +161,22 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
106
161
  // 触发 rc-cascader 重新构建 pathKeyEntities,避免二级节点未被索引导致的报错。
107
162
  const options = useMemo(() => {
108
163
  if (!resolvedMetaTree) return [];
164
+ const refreshSeq = updateFlag;
109
165
  const base = buildContextSelectorItems(resolvedMetaTree);
110
- return translateOptions(base).filter((item) => {
166
+ const filtered = translateOptions(base).filter((item) => {
111
167
  if (!ignoreFieldNames || ignoreFieldNames.length === 0) return true;
112
168
  return !ignoreFieldNames.includes(item.meta?.name || '');
113
169
  });
170
+ return refreshSeq >= 0 ? filtered : [];
114
171
  }, [resolvedMetaTree, updateFlag, translateOptions, ignoreFieldNames]);
115
172
 
173
+ const displayOptions = useMemo(() => {
174
+ if (!isSearchEnabled || !searchText.trim()) {
175
+ return options;
176
+ }
177
+ return filterLoadedContextSelectorItems(options, searchText);
178
+ }, [isSearchEnabled, options, searchText]);
179
+
116
180
  // 内部展开路径:在 onlyLeafSelectable=true 时,点击父节点不会触发 onChange,
117
181
  // 但会触发 loadData。我们在此记录路径以在懒加载后保持展开。
118
182
  const [tempSelectedPath, setTempSelectedPath] = useState<string[]>([]);
@@ -158,23 +222,36 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
158
222
  triggerUpdate();
159
223
  }
160
224
  },
161
- [triggerUpdate],
225
+ [triggerUpdate, translateOptions],
162
226
  );
163
227
 
164
228
  const currentPath = useMemo(() => {
165
- return customParseValueToPath(value);
229
+ return normalizePath(customParseValueToPath(value));
166
230
  }, [value, customParseValueToPath]);
167
231
 
168
232
  // 当 metaTree 为子层(如 getPropertyMetaTree('{{ ctx.collection }}') 返回的是 collection 的子节点)
169
233
  // 而 value path 仍包含根键(如 ['collection', 'field'])时,自动丢弃不存在的首段,确保级联能正确对齐。
170
234
  const effectivePath = useMemo(() => {
171
235
  if (!currentPath || currentPath.length === 0) return currentPath;
236
+
237
+ if (options.length === 0) {
238
+ return currentPath;
239
+ }
240
+
172
241
  const topValues = new Set(options.map((o) => String(o.value)));
173
242
  const needTrim = !topValues.has(String(currentPath[0]));
174
243
  const fixed = needTrim ? currentPath.slice(1) : currentPath;
175
244
  return fixed;
176
245
  }, [currentPath, options]);
177
246
 
247
+ const cascaderValue = useMemo(() => {
248
+ if (tempSelectedPath.length > 0) {
249
+ return tempSelectedPath;
250
+ }
251
+
252
+ return Array.isArray(effectivePath) ? effectivePath : undefined;
253
+ }, [effectivePath, tempSelectedPath]);
254
+
178
255
  // 预加载:当存在有效路径时,按路径逐级加载 children,保证默认展开和选中路径可用
179
256
  const pathToPreload = useMemo(() => {
180
257
  const finalPath = effectivePath && effectivePath.length > 0 ? effectivePath : tempSelectedPath;
@@ -251,21 +328,140 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
251
328
  return cx(cascaderPopupAutoHeightClassName, cascaderProps.popupClassName);
252
329
  }, [cascaderProps.popupClassName]);
253
330
 
331
+ const cascaderSearchInputClassName = useMemo(() => {
332
+ return css`
333
+ padding: 8px;
334
+ border-bottom: 1px solid ${token.colorSplit};
335
+ `;
336
+ }, [token.colorSplit]);
337
+
338
+ const {
339
+ onDropdownVisibleChange: cascaderOnDropdownVisibleChange,
340
+ dropdownRender: cascaderDropdownRender,
341
+ ...restCascaderProps
342
+ } = cascaderProps;
343
+
344
+ const selectedPathInfo = useMemo(() => getSelectedPathInfo(effectivePath, options), [effectivePath, options]);
345
+
346
+ const mergedOpen = open !== undefined ? open : children === null ? dropdownOpen : undefined;
347
+
348
+ const isDropdownVisible = !!mergedOpen;
349
+
350
+ const handleDropdownVisibleChange = useCallback(
351
+ (visible: boolean) => {
352
+ if (open === undefined) {
353
+ setDropdownOpen(visible);
354
+ }
355
+ if (!visible) {
356
+ setSearchText('');
357
+ }
358
+ cascaderOnDropdownVisibleChange?.(visible);
359
+ },
360
+ [cascaderOnDropdownVisibleChange, open],
361
+ );
362
+
363
+ const renderDropdown = useCallback(
364
+ (menu: React.ReactElement) => {
365
+ const cascaderMenuNode = cascaderDropdownRender ? cascaderDropdownRender(menu) : menu;
366
+ const cascaderMenu = React.isValidElement(cascaderMenuNode) ? cascaderMenuNode : <>{cascaderMenuNode}</>;
367
+ if (!isSearchEnabled || children === null) {
368
+ return cascaderMenu;
369
+ }
370
+
371
+ return (
372
+ <>
373
+ <div className={cascaderSearchInputClassName}>
374
+ <Input
375
+ allowClear
376
+ size="small"
377
+ value={searchText}
378
+ placeholder={flowCtx.t('Search')}
379
+ onChange={(e) => setSearchText(e.target.value)}
380
+ onKeyDown={(e) => e.stopPropagation()}
381
+ />
382
+ </div>
383
+ {cascaderMenu}
384
+ </>
385
+ );
386
+ },
387
+ [cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText],
388
+ );
389
+
390
+ const inlinePlaceholder =
391
+ typeof restCascaderProps.placeholder === 'string' ? restCascaderProps.placeholder : flowCtx.t('Search');
392
+ const hasSelectedPath = Array.isArray(effectivePath) && effectivePath.length > 0;
393
+
394
+ const handleInlineInputFocus = useCallback(() => {
395
+ if (open === undefined && !inlineFocusByPointerRef.current) {
396
+ setDropdownOpen(true);
397
+ }
398
+ }, [open]);
399
+
400
+ const markInlineFocusByPointer = useCallback(() => {
401
+ inlineFocusByPointerRef.current = true;
402
+ }, []);
403
+
404
+ const resetInlineFocusByPointer = useCallback(() => {
405
+ inlineFocusByPointerRef.current = false;
406
+ }, []);
407
+
408
+ const handleInlineInputChange = useCallback(
409
+ (event: React.ChangeEvent<HTMLInputElement>) => {
410
+ const nextValue = event.target.value;
411
+
412
+ // 下拉关闭态下点击清空:应清空真实已选值,而不是仅清空搜索词。
413
+ if (!isDropdownVisible && nextValue === '' && hasSelectedPath) {
414
+ setTempSelectedPath([]);
415
+ // 清空语义:传空 meta,确保上层(如 VariableInput)进入 clear 分支。
416
+ onChange?.('', undefined);
417
+ return;
418
+ }
419
+
420
+ if (open === undefined && !isDropdownVisible) {
421
+ setDropdownOpen(true);
422
+ }
423
+
424
+ setSearchText(nextValue);
425
+ },
426
+ [hasSelectedPath, isDropdownVisible, onChange, open],
427
+ );
428
+
429
+ const inlinePathText = Array.isArray(effectivePath) ? effectivePath.join(' / ') : '';
430
+ const inlineInputValue = isDropdownVisible ? searchText : selectedPathInfo.text || inlinePathText;
431
+
254
432
  return (
255
433
  <Cascader
256
- {...cascaderProps}
257
- options={options}
258
- value={tempSelectedPath && tempSelectedPath.length > 0 ? tempSelectedPath : effectivePath}
434
+ {...restCascaderProps}
435
+ options={displayOptions}
436
+ value={cascaderValue}
259
437
  onChange={handleChange}
260
438
  loadData={handleLoadData}
261
439
  loading={loading}
262
440
  changeOnSelect={!onlyLeafSelectable}
263
441
  expandTrigger="click"
264
- open={open}
265
- showSearch={children === null}
442
+ open={mergedOpen}
443
+ showSearch={false}
266
444
  popupClassName={mergedPopupClassName}
445
+ dropdownRender={renderDropdown}
446
+ onDropdownVisibleChange={handleDropdownVisibleChange}
267
447
  >
268
- {children === null ? null : children || defaultChildren}
448
+ {children === null ? (
449
+ <Input
450
+ allowClear
451
+ value={inlineInputValue}
452
+ placeholder={inlinePlaceholder}
453
+ onMouseDown={markInlineFocusByPointer}
454
+ onMouseUp={resetInlineFocusByPointer}
455
+ onMouseLeave={resetInlineFocusByPointer}
456
+ onFocus={handleInlineInputFocus}
457
+ onBlur={resetInlineFocusByPointer}
458
+ onChange={handleInlineInputChange}
459
+ onKeyDown={(e) => e.stopPropagation()}
460
+ disabled={restCascaderProps.disabled}
461
+ />
462
+ ) : (
463
+ children || defaultChildren
464
+ )}
269
465
  </Cascader>
270
466
  );
271
467
  };
@@ -8,11 +8,13 @@
8
8
  */
9
9
 
10
10
  import { ConfigProvider } from 'antd';
11
- import { Popup } from 'antd-mobile';
12
11
  import React, { FC, ReactNode, useMemo } from 'react';
13
- import { CloseOutline } from 'antd-mobile-icons';
14
12
  import { useMobileActionDrawerStyle } from './MobilePopup.style';
15
13
  import { useTranslation } from 'react-i18next';
14
+ import { lazy } from '../lazy-helper';
15
+
16
+ const { Popup } = lazy(() => import('antd-mobile'), 'Popup');
17
+ const { CloseOutline } = lazy(() => import('antd-mobile-icons'), 'CloseOutline');
16
18
 
17
19
  interface MobilePopupProps {
18
20
  title?: string;
@@ -114,7 +114,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
114
114
  }
115
115
 
116
116
  const engine = new FlowEngine();
117
- engine.flowSettings.forceEnable();
117
+ await engine.flowSettings.forceEnable();
118
118
  engine.registerModels({ BrokenModel });
119
119
  const model = engine.createModel({ use: 'BrokenModel', uid: 'broken-top-2' }) as BrokenModel;
120
120
  // satisfy FlowsFloatContextMenu styles
@@ -154,7 +154,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
154
154
  }
155
155
 
156
156
  const engine = new FlowEngine();
157
- engine.flowSettings.forceEnable();
157
+ await engine.flowSettings.forceEnable();
158
158
  engine.registerModels({ ParentModel, BrokenChild });
159
159
  const parent = engine.createModel({ use: 'ParentModel', uid: 'parent-3' }) as ParentModel;
160
160
  const child = engine.createModel({ use: 'BrokenChild', uid: 'child-3' }) as BrokenChild;
@@ -200,7 +200,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
200
200
  }
201
201
 
202
202
  const engine = new FlowEngine();
203
- engine.flowSettings.forceEnable();
203
+ await engine.flowSettings.forceEnable();
204
204
  engine.registerModels({ ParentModel, RenderFnChild });
205
205
  const parent = engine.createModel({ use: 'ParentModel', uid: 'parent-4' }) as ParentModel;
206
206
  const child = engine.createModel({ use: 'RenderFnChild', uid: 'cell-4' }) as RenderFnChild;
@@ -15,13 +15,106 @@ import {
15
15
  getSlotKey,
16
16
  resolveDropIntent,
17
17
  Point,
18
+ buildLayoutSnapshot,
18
19
  } from '../dnd/gridDragPlanner';
19
20
 
20
21
  const rect = { top: 0, left: 0, width: 100, height: 100 };
21
22
 
22
- const createLayout = (rows: Record<string, string[][]>, sizes: Record<string, number[]>): GridLayoutData => ({
23
+ const createLayout = (
24
+ rows: Record<string, string[][]>,
25
+ sizes: Record<string, number[]>,
26
+ rowOrder?: string[],
27
+ ): GridLayoutData => ({
23
28
  rows,
24
29
  sizes,
30
+ rowOrder,
31
+ });
32
+
33
+ const createDomRect = ({ top, left, width, height }: { top: number; left: number; width: number; height: number }) => {
34
+ return {
35
+ top,
36
+ left,
37
+ width,
38
+ height,
39
+ right: left + width,
40
+ bottom: top + height,
41
+ x: left,
42
+ y: top,
43
+ toJSON: () => ({}),
44
+ } as DOMRect;
45
+ };
46
+
47
+ const mockRect = (
48
+ element: Element,
49
+ rect: {
50
+ top: number;
51
+ left: number;
52
+ width: number;
53
+ height: number;
54
+ },
55
+ ) => {
56
+ Object.defineProperty(element, 'getBoundingClientRect', {
57
+ configurable: true,
58
+ value: () => createDomRect(rect),
59
+ });
60
+ };
61
+
62
+ describe('buildLayoutSnapshot', () => {
63
+ it('should ignore nested grid columns/items even when rowId is duplicated', () => {
64
+ const container = document.createElement('div');
65
+ const row = document.createElement('div');
66
+ row.setAttribute('data-grid-row-id', 'row-1');
67
+ container.appendChild(row);
68
+
69
+ const column = document.createElement('div');
70
+ column.setAttribute('data-grid-column-row-id', 'row-1');
71
+ column.setAttribute('data-grid-column-index', '0');
72
+ row.appendChild(column);
73
+
74
+ const item = document.createElement('div');
75
+ item.setAttribute('data-grid-item-row-id', 'row-1');
76
+ item.setAttribute('data-grid-column-index', '0');
77
+ item.setAttribute('data-grid-item-index', '0');
78
+ column.appendChild(item);
79
+
80
+ // 在外层 item 内构建一个嵌套 grid,并复用相同 rowId/columnIndex
81
+ const nestedRow = document.createElement('div');
82
+ nestedRow.setAttribute('data-grid-row-id', 'row-1');
83
+ item.appendChild(nestedRow);
84
+
85
+ const nestedColumn = document.createElement('div');
86
+ nestedColumn.setAttribute('data-grid-column-row-id', 'row-1');
87
+ nestedColumn.setAttribute('data-grid-column-index', '0');
88
+ nestedRow.appendChild(nestedColumn);
89
+
90
+ const nestedItem = document.createElement('div');
91
+ nestedItem.setAttribute('data-grid-item-row-id', 'row-1');
92
+ nestedItem.setAttribute('data-grid-column-index', '0');
93
+ nestedItem.setAttribute('data-grid-item-index', '0');
94
+ nestedColumn.appendChild(nestedItem);
95
+
96
+ mockRect(container, { top: 0, left: 0, width: 600, height: 600 });
97
+ mockRect(row, { top: 10, left: 10, width: 320, height: 120 });
98
+ mockRect(column, { top: 10, left: 10, width: 320, height: 120 });
99
+ mockRect(item, { top: 20, left: 20, width: 300, height: 80 });
100
+
101
+ // 嵌套 grid 给一个明显偏离的位置,用于判断是否被错误命中
102
+ mockRect(nestedRow, { top: 360, left: 360, width: 200, height: 120 });
103
+ mockRect(nestedColumn, { top: 360, left: 360, width: 200, height: 120 });
104
+ mockRect(nestedItem, { top: 370, left: 370, width: 180, height: 90 });
105
+
106
+ const snapshot = buildLayoutSnapshot({ container });
107
+ const columnEdgeSlots = snapshot.slots.filter((slot) => slot.type === 'column-edge');
108
+ const columnSlots = snapshot.slots.filter((slot) => slot.type === 'column');
109
+
110
+ // 外层单行单列单项应只有 6 个 slot:上/下 row-gap + 左/右 column-edge + before/after column
111
+ expect(snapshot.slots).toHaveLength(6);
112
+ expect(columnEdgeSlots).toHaveLength(2);
113
+ expect(columnSlots).toHaveLength(2);
114
+
115
+ // 不应混入嵌套 grid(其 top >= 360)
116
+ expect(snapshot.slots.every((slot) => slot.rect.top < 300)).toBe(true);
117
+ });
25
118
  });
26
119
 
27
120
  describe('getSlotKey', () => {
@@ -275,6 +368,7 @@ describe('simulateLayoutForSlot', () => {
275
368
  rowA: [24],
276
369
  rowB: [24],
277
370
  },
371
+ ['rowA', 'rowB'],
278
372
  );
279
373
 
280
374
  const slot: LayoutSlot = {
@@ -315,6 +409,33 @@ describe('simulateLayoutForSlot', () => {
315
409
  expect(result.sizes['row-new']).toEqual([24]);
316
410
  });
317
411
 
412
+ it('removes empty source row when moving item into empty container slot', () => {
413
+ const layout = createLayout(
414
+ {
415
+ rowA: [['block-x']],
416
+ },
417
+ {
418
+ rowA: [24],
419
+ },
420
+ );
421
+
422
+ const slot: LayoutSlot = {
423
+ type: 'empty-row',
424
+ rect,
425
+ };
426
+
427
+ const result = simulateLayoutForSlot({
428
+ slot,
429
+ sourceUid: 'block-x',
430
+ layout,
431
+ generateRowId: () => 'row-new',
432
+ });
433
+
434
+ expect(result.rows['row-new']).toEqual([['block-x']]);
435
+ expect(result.rows.rowA).toBeUndefined();
436
+ expect(result.sizes.rowA).toBeUndefined();
437
+ });
438
+
318
439
  it('handles column slot with after position', () => {
319
440
  const layout = createLayout(
320
441
  {
@@ -373,6 +494,7 @@ describe('simulateLayoutForSlot', () => {
373
494
  rowA: [24],
374
495
  rowB: [24],
375
496
  },
497
+ ['rowA', 'rowB'],
376
498
  );
377
499
 
378
500
  const slot: LayoutSlot = {
@@ -392,6 +514,112 @@ describe('simulateLayoutForSlot', () => {
392
514
  expect(Object.keys(result.rows)).toEqual(['rowA', 'row-inserted', 'rowB']);
393
515
  });
394
516
 
517
+ it('inserts row into rowOrder when dropping below target row', () => {
518
+ const layout = createLayout(
519
+ {
520
+ rowA: [['a']],
521
+ rowB: [['b']],
522
+ },
523
+ {
524
+ rowA: [24],
525
+ rowB: [24],
526
+ },
527
+ ['rowA', 'rowB'],
528
+ );
529
+
530
+ const slot: LayoutSlot = {
531
+ type: 'row-gap',
532
+ targetRowId: 'rowA',
533
+ position: 'below',
534
+ rect,
535
+ };
536
+
537
+ const result = simulateLayoutForSlot({
538
+ slot,
539
+ sourceUid: 'c',
540
+ layout,
541
+ generateRowId: () => 'row-new',
542
+ });
543
+
544
+ expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
545
+ });
546
+
547
+ it('maintains rowOrder and inserts new row before target when provided', () => {
548
+ const layout = createLayout(
549
+ {
550
+ rowA: [['a']],
551
+ rowB: [['b']],
552
+ },
553
+ {
554
+ rowA: [24],
555
+ rowB: [24],
556
+ },
557
+ ['rowA', 'rowB'],
558
+ );
559
+
560
+ const slot: LayoutSlot = {
561
+ type: 'row-gap',
562
+ targetRowId: 'rowB',
563
+ position: 'above',
564
+ rect,
565
+ };
566
+
567
+ const result = simulateLayoutForSlot({
568
+ slot,
569
+ sourceUid: 'c',
570
+ layout,
571
+ generateRowId: () => 'row-new',
572
+ });
573
+
574
+ expect(result.rowOrder).toEqual(['rowA', 'row-new', 'rowB']);
575
+ expect(result.rows).toEqual({
576
+ rowA: [['a']],
577
+ 'row-new': [['c']],
578
+ rowB: [['b']],
579
+ });
580
+ expect(result.sizes).toEqual({
581
+ rowA: [24],
582
+ 'row-new': [24],
583
+ rowB: [24],
584
+ });
585
+ });
586
+
587
+ it('derives rowOrder from rows when missing and removes empty rows from order', () => {
588
+ const layout = createLayout(
589
+ {
590
+ row1: [['a']],
591
+ row2: [['b']],
592
+ row3: [['c']],
593
+ },
594
+ {
595
+ row1: [24],
596
+ row2: [24],
597
+ row3: [24],
598
+ },
599
+ );
600
+
601
+ const slot: LayoutSlot = {
602
+ type: 'column',
603
+ rowId: 'row1',
604
+ columnIndex: 0,
605
+ insertIndex: 0,
606
+ position: 'before',
607
+ rect,
608
+ };
609
+
610
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'b', layout });
611
+
612
+ expect(result.rowOrder).toEqual(['row1', 'row3']);
613
+ expect(result.rows).toEqual({
614
+ row1: [['b', 'a']],
615
+ row3: [['c']],
616
+ });
617
+ expect(result.sizes).toEqual({
618
+ row1: [24],
619
+ row3: [24],
620
+ });
621
+ });
622
+
395
623
  it('handles empty-column slot by replacing empty column', () => {
396
624
  const layout = createLayout(
397
625
  {