@nocobase/flow-engine 2.0.0-beta.9 → 2.0.0

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 (245) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/FlowDefinition.d.ts +2 -0
  3. package/lib/JSRunner.d.ts +6 -0
  4. package/lib/JSRunner.js +32 -2
  5. package/lib/ViewScopedFlowEngine.js +3 -0
  6. package/lib/acl/Acl.js +13 -3
  7. package/lib/components/FlowContextSelector.js +155 -10
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  10. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  11. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
  12. package/lib/components/variables/VariableInput.js +9 -4
  13. package/lib/components/variables/VariableTag.js +46 -39
  14. package/lib/components/variables/utils.d.ts +7 -0
  15. package/lib/components/variables/utils.js +42 -2
  16. package/lib/data-source/index.d.ts +7 -27
  17. package/lib/data-source/index.js +81 -51
  18. package/lib/executor/FlowExecutor.d.ts +2 -1
  19. package/lib/executor/FlowExecutor.js +163 -22
  20. package/lib/flowContext.d.ts +230 -7
  21. package/lib/flowContext.js +2267 -148
  22. package/lib/flowEngine.d.ts +21 -0
  23. package/lib/flowEngine.js +56 -8
  24. package/lib/flowI18n.js +6 -4
  25. package/lib/flowSettings.js +17 -11
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +21 -0
  28. package/lib/locale/en-US.json +9 -2
  29. package/lib/locale/index.d.ts +14 -0
  30. package/lib/locale/zh-CN.json +8 -1
  31. package/lib/models/CollectionFieldModel.d.ts +1 -0
  32. package/lib/models/CollectionFieldModel.js +3 -2
  33. package/lib/models/flowModel.js +12 -1
  34. package/lib/provider.js +5 -5
  35. package/lib/resources/baseRecordResource.d.ts +5 -0
  36. package/lib/resources/baseRecordResource.js +24 -0
  37. package/lib/resources/multiRecordResource.d.ts +1 -0
  38. package/lib/resources/multiRecordResource.js +11 -4
  39. package/lib/resources/singleRecordResource.js +2 -0
  40. package/lib/resources/sqlResource.d.ts +4 -3
  41. package/lib/resources/sqlResource.js +8 -3
  42. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  43. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  44. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  45. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  46. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  47. package/lib/runjs-context/contexts/base.js +706 -41
  48. package/lib/runjs-context/contributions.d.ts +33 -0
  49. package/lib/runjs-context/contributions.js +88 -0
  50. package/lib/runjs-context/helpers.js +12 -1
  51. package/lib/runjs-context/setup.js +6 -0
  52. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  53. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  54. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  55. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  56. package/lib/runjs-context/snippets/index.d.ts +11 -1
  57. package/lib/runjs-context/snippets/index.js +61 -40
  58. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  59. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  60. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  61. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  62. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  63. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  64. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  65. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  66. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  67. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  68. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  69. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  70. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  71. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  72. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  73. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  74. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  75. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  76. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  78. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  79. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  80. package/lib/runjsLibs.d.ts +28 -0
  81. package/lib/runjsLibs.js +532 -0
  82. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  83. package/lib/scheduler/ModelOperationScheduler.js +25 -21
  84. package/lib/types.d.ts +27 -0
  85. package/lib/utils/associationObjectVariable.d.ts +2 -2
  86. package/lib/utils/createCollectionContextMeta.js +1 -0
  87. package/lib/utils/createEphemeralContext.js +2 -2
  88. package/lib/utils/dateVariable.d.ts +16 -0
  89. package/lib/utils/dateVariable.js +380 -0
  90. package/lib/utils/exceptions.d.ts +7 -0
  91. package/lib/utils/exceptions.js +10 -0
  92. package/lib/utils/index.d.ts +8 -3
  93. package/lib/utils/index.js +45 -0
  94. package/lib/utils/params-resolvers.js +16 -9
  95. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  96. package/lib/utils/resolveModuleUrl.js +65 -0
  97. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  98. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  99. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  100. package/lib/utils/runjsModuleLoader.js +422 -0
  101. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  102. package/lib/utils/runjsTemplateCompat.js +743 -0
  103. package/lib/utils/runjsValue.d.ts +29 -0
  104. package/lib/utils/runjsValue.js +275 -0
  105. package/lib/utils/safeGlobals.d.ts +18 -8
  106. package/lib/utils/safeGlobals.js +164 -17
  107. package/lib/utils/schema-utils.d.ts +10 -0
  108. package/lib/utils/schema-utils.js +61 -0
  109. package/lib/views/createViewMeta.d.ts +0 -7
  110. package/lib/views/createViewMeta.js +19 -70
  111. package/lib/views/index.d.ts +1 -2
  112. package/lib/views/index.js +4 -3
  113. package/lib/views/useDialog.js +7 -2
  114. package/lib/views/useDrawer.js +7 -2
  115. package/lib/views/usePage.d.ts +4 -0
  116. package/lib/views/usePage.js +43 -6
  117. package/lib/views/usePopover.js +4 -1
  118. package/lib/views/viewEvents.d.ts +17 -0
  119. package/lib/views/viewEvents.js +90 -0
  120. package/package.json +4 -4
  121. package/src/BlockScopedFlowEngine.ts +2 -5
  122. package/src/JSRunner.ts +44 -2
  123. package/src/ViewScopedFlowEngine.ts +4 -0
  124. package/src/__tests__/JSRunner.test.ts +64 -0
  125. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  126. package/src/__tests__/flowContext.test.ts +693 -1
  127. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  128. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  129. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  130. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  131. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  132. package/src/__tests__/runjsContext.test.ts +10 -7
  133. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  134. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  135. package/src/__tests__/runjsContributions.test.ts +89 -0
  136. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  137. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  138. package/src/__tests__/runjsLocales.test.ts +4 -1
  139. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  140. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  141. package/src/__tests__/runjsSnippets.test.ts +40 -3
  142. package/src/acl/Acl.tsx +3 -3
  143. package/src/components/FlowContextSelector.tsx +208 -12
  144. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  145. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  146. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  147. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  148. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
  149. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  150. package/src/components/variables/VariableInput.tsx +12 -4
  151. package/src/components/variables/VariableTag.tsx +54 -45
  152. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  153. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  154. package/src/components/variables/__tests__/utils.test.ts +81 -3
  155. package/src/components/variables/utils.ts +67 -6
  156. package/src/data-source/index.ts +85 -110
  157. package/src/executor/FlowExecutor.ts +200 -23
  158. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  159. package/src/flowContext.ts +2986 -211
  160. package/src/flowEngine.ts +59 -8
  161. package/src/flowI18n.ts +7 -5
  162. package/src/flowSettings.ts +18 -12
  163. package/src/index.ts +14 -1
  164. package/src/locale/en-US.json +9 -2
  165. package/src/locale/zh-CN.json +8 -1
  166. package/src/models/CollectionFieldModel.tsx +3 -1
  167. package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
  168. package/src/models/__tests__/flowModel.test.ts +20 -4
  169. package/src/models/flowModel.tsx +13 -1
  170. package/src/provider.tsx +7 -6
  171. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  172. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  173. package/src/resources/baseRecordResource.ts +31 -0
  174. package/src/resources/multiRecordResource.ts +11 -4
  175. package/src/resources/singleRecordResource.ts +3 -0
  176. package/src/resources/sqlResource.ts +11 -6
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  179. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  180. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  181. package/src/runjs-context/contexts/base.ts +715 -44
  182. package/src/runjs-context/contributions.ts +88 -0
  183. package/src/runjs-context/helpers.ts +11 -1
  184. package/src/runjs-context/setup.ts +6 -0
  185. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  186. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  187. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  188. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  189. package/src/runjs-context/snippets/index.ts +75 -41
  190. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  191. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  192. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  193. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  194. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  195. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  196. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  197. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  198. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  199. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  200. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  201. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  202. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  203. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  204. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  205. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  206. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  207. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  208. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  209. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  210. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  211. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  212. package/src/runjsLibs.ts +622 -0
  213. package/src/scheduler/ModelOperationScheduler.ts +27 -21
  214. package/src/types.ts +38 -1
  215. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  216. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  217. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  218. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  219. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  220. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  221. package/src/utils/__tests__/utils.test.ts +95 -0
  222. package/src/utils/associationObjectVariable.ts +2 -2
  223. package/src/utils/createCollectionContextMeta.ts +1 -0
  224. package/src/utils/createEphemeralContext.ts +5 -4
  225. package/src/utils/dateVariable.ts +397 -0
  226. package/src/utils/exceptions.ts +11 -0
  227. package/src/utils/index.ts +37 -3
  228. package/src/utils/params-resolvers.ts +23 -9
  229. package/src/utils/resolveModuleUrl.ts +91 -0
  230. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  231. package/src/utils/runjsModuleLoader.ts +553 -0
  232. package/src/utils/runjsTemplateCompat.ts +828 -0
  233. package/src/utils/runjsValue.ts +287 -0
  234. package/src/utils/safeGlobals.ts +188 -17
  235. package/src/utils/schema-utils.ts +79 -0
  236. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  237. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  238. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  239. package/src/views/createViewMeta.ts +22 -75
  240. package/src/views/index.tsx +1 -2
  241. package/src/views/useDialog.tsx +8 -1
  242. package/src/views/useDrawer.tsx +8 -1
  243. package/src/views/usePage.tsx +51 -5
  244. package/src/views/usePopover.tsx +4 -1
  245. 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
  };
@@ -52,7 +52,8 @@ export const SwitchWithTitle: FC = observer(
52
52
  };
53
53
 
54
54
  // 点击整个容器时触发
55
- const handleWrapperClick = () => {
55
+ const handleWrapperClick = (e: React.MouseEvent) => {
56
+ e.stopPropagation();
56
57
  if (disabled) return;
57
58
  handleChange(!checked);
58
59
  };
@@ -0,0 +1,74 @@
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 { describe, it, expect, vi } from 'vitest';
12
+ import { render, screen, fireEvent } from '@nocobase/test/client';
13
+ import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
14
+
15
+ import { SwitchWithTitle } from '../SwitchWithTitle';
16
+ import { SelectWithTitle } from '../SelectWithTitle';
17
+
18
+ vi.mock('antd', async (importOriginal) => {
19
+ const actual = (await importOriginal()) as any;
20
+ return {
21
+ ...actual,
22
+ Select: ({
23
+ popupMatchSelectWidth,
24
+ bordered,
25
+ popupClassName,
26
+ fieldNames,
27
+ labelRender,
28
+ optionRender,
29
+ dropdownRender,
30
+ options,
31
+ ...props
32
+ }: any) => React.createElement('select', props),
33
+ Switch: ({ checkedChildren, unCheckedChildren, size, ...props }: any) =>
34
+ React.createElement('input', { ...props, type: 'checkbox', readOnly: true }),
35
+ };
36
+ });
37
+
38
+ describe('Inline controls - stopPropagation', () => {
39
+ it('SwitchWithTitle click does not bubble to parent', async () => {
40
+ const engine = new FlowEngine();
41
+ const parentClick = vi.fn();
42
+ const onChange = vi.fn();
43
+
44
+ render(
45
+ <FlowEngineProvider engine={engine}>
46
+ <div onClick={parentClick}>
47
+ <SwitchWithTitle title="Enabled" itemKey="enabled" onChange={onChange} />
48
+ </div>
49
+ </FlowEngineProvider>,
50
+ );
51
+
52
+ fireEvent.click(screen.getByText('Enabled'));
53
+
54
+ expect(parentClick).not.toHaveBeenCalled();
55
+ expect(onChange).toHaveBeenCalledWith({ enabled: true });
56
+ });
57
+
58
+ it('SelectWithTitle click does not bubble to parent', async () => {
59
+ const engine = new FlowEngine();
60
+ const parentClick = vi.fn();
61
+
62
+ render(
63
+ <FlowEngineProvider engine={engine}>
64
+ <div onClick={parentClick}>
65
+ <SelectWithTitle title="Mode" itemKey="mode" options={[{ label: 'A', value: 'a' }]} />
66
+ </div>
67
+ </FlowEngineProvider>,
68
+ );
69
+
70
+ fireEvent.click(screen.getByText('Mode'));
71
+
72
+ expect(parentClick).not.toHaveBeenCalled();
73
+ });
74
+ });
@@ -7,9 +7,9 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { ExclamationCircleOutlined, MenuOutlined } from '@ant-design/icons';
10
+ import { ExclamationCircleOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
11
11
  import type { DropdownProps, MenuProps } from 'antd';
12
- import { App, Dropdown, Modal } from 'antd';
12
+ import { App, Dropdown, Modal, Tooltip, theme } from 'antd';
13
13
  import React, { startTransition, useCallback, useEffect, useMemo, useState, FC } from 'react';
14
14
  import { FlowModel } from '../../../../models';
15
15
  import type { FlowModelExtraMenuItem } from '../../../../models';
@@ -17,6 +17,7 @@ import type { StepDefinition, StepUIMode } from '../../../../types';
17
17
  import {
18
18
  getT,
19
19
  resolveStepUiSchema,
20
+ resolveStepDisabledInSettings,
20
21
  shouldHideStepInSettings,
21
22
  resolveDefaultParams,
22
23
  resolveUiMode,
@@ -33,6 +34,8 @@ interface StepInfo {
33
34
  title: string;
34
35
  modelKey?: string;
35
36
  uiMode?: StepUIMode;
37
+ disabled?: boolean;
38
+ disabledReason?: string;
36
39
  }
37
40
 
38
41
  interface FlowInfo {
@@ -150,12 +153,29 @@ const componentMap = {
150
153
  const MenuLabelItem = ({ title, uiMode, itemProps }) => {
151
154
  const type = uiMode?.type || uiMode;
152
155
  const Component = type ? componentMap[type] : null;
156
+ const disabled = !!itemProps?.disabled;
157
+ const disabledReason = itemProps?.disabledReason;
158
+ const disabledIconColor = itemProps?.disabledIconColor;
153
159
 
154
- if (!Component) {
155
- return <>{title}</>;
160
+ const content = (() => {
161
+ if (!Component) {
162
+ return <>{title}</>;
163
+ }
164
+ return <Component title={title} {...itemProps} />;
165
+ })();
166
+
167
+ if (!disabled) {
168
+ return content;
156
169
  }
157
170
 
158
- return <Component title={title} {...itemProps} />;
171
+ return (
172
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
173
+ {content}
174
+ <Tooltip title={disabledReason} placement="right" destroyTooltipOnHide>
175
+ <QuestionCircleOutlined style={{ color: disabledIconColor }} />
176
+ </Tooltip>
177
+ </span>
178
+ );
159
179
  };
160
180
 
161
181
  /**
@@ -180,10 +200,17 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
180
200
  }) => {
181
201
  const { message } = App.useApp();
182
202
  const t = useMemo(() => getT(model), [model]);
203
+ const { token } = theme.useToken();
204
+ const disabledIconColor = token?.colorTextTertiary || token?.colorTextDescription || token?.colorTextSecondary;
183
205
  const [visible, setVisible] = useState(false);
184
206
  // 当模型发生子模型替换/增删等变化时,强制刷新菜单数据
185
207
  const [refreshTick, setRefreshTick] = useState(0);
186
208
  const [extraMenuItems, setExtraMenuItems] = useState<FlowModelExtraMenuItem[]>([]);
209
+ const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = useState<FlowInfo[]>([]);
210
+ const [isLoading, setIsLoading] = useState(true);
211
+ const closeDropdown = useCallback(() => {
212
+ setVisible(false);
213
+ }, []);
187
214
  const handleOpenChange: DropdownProps['onOpenChange'] = useCallback((nextOpen: boolean, info) => {
188
215
  if (info.source === 'trigger' || nextOpen) {
189
216
  // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
@@ -234,7 +261,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
234
261
  return () => {
235
262
  mounted = false;
236
263
  };
237
- }, [model, menuLevels, t, refreshTick, visible, message]);
264
+ }, [model, menuLevels, t, refreshTick, visible]);
238
265
 
239
266
  // 统一的复制 UID 方法
240
267
  const copyUidToClipboard = useCallback(
@@ -292,6 +319,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
292
319
  );
293
320
 
294
321
  const handleDelete = useCallback(() => {
322
+ closeDropdown();
295
323
  Modal.confirm({
296
324
  title: t('Confirm delete'),
297
325
  icon: <ExclamationCircleOutlined />,
@@ -312,7 +340,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
312
340
  }
313
341
  },
314
342
  });
315
- }, [model]);
343
+ }, [closeDropdown, model, t]);
316
344
 
317
345
  const handleStepConfiguration = useCallback(
318
346
  (key: string) => {
@@ -345,6 +373,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
345
373
  }
346
374
 
347
375
  try {
376
+ closeDropdown();
348
377
  targetModel.openFlowSettings({
349
378
  flowKey,
350
379
  stepKey,
@@ -353,7 +382,32 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
353
382
  console.log(t('Configuration popup cancelled or error'), ':', error);
354
383
  }
355
384
  },
356
- [model],
385
+ [closeDropdown, model, t],
386
+ );
387
+
388
+ const isStepMenuItemDisabled = useCallback(
389
+ (key: string) => {
390
+ const cleanKey = key.includes('-') && /^(.+)-\d+$/.test(key) ? key.replace(/-\d+$/, '') : key;
391
+ const keys = cleanKey.split(':');
392
+ let modelKey: string | undefined;
393
+ let flowKey: string | undefined;
394
+ let stepKey: string | undefined;
395
+
396
+ if (keys.length === 3) {
397
+ [modelKey, flowKey, stepKey] = keys;
398
+ } else if (keys.length === 2) {
399
+ [flowKey, stepKey] = keys;
400
+ } else {
401
+ return false;
402
+ }
403
+
404
+ return configurableFlowsAndSteps.some(({ flow, steps, modelKey: flowModelKey }: FlowInfo) => {
405
+ const sameModel = (flowModelKey || undefined) === modelKey;
406
+ if (!sameModel || flow.key !== flowKey) return false;
407
+ return steps.some((stepInfo: StepInfo) => stepInfo.stepKey === stepKey && !!stepInfo.disabled);
408
+ });
409
+ },
410
+ [configurableFlowsAndSteps],
357
411
  );
358
412
 
359
413
  const handleMenuClick = useCallback(
@@ -363,18 +417,25 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
363
417
  const cleanKey = key.includes('-') && /^(.+)-\d+$/.test(key) ? key.replace(/-\d+$/, '') : key;
364
418
 
365
419
  if (cleanKey.startsWith('copy-pop-uid:')) {
420
+ closeDropdown();
366
421
  handleCopyPopupUid(cleanKey);
367
422
  return;
368
423
  }
369
424
 
370
425
  const extra = extraMenuItems.find((it) => it?.key === originalKey || it?.key === cleanKey);
371
426
  if (extra?.onClick) {
427
+ closeDropdown();
372
428
  extra.onClick();
373
429
  return;
374
430
  }
375
431
 
432
+ if (isStepMenuItemDisabled(cleanKey)) {
433
+ return;
434
+ }
435
+
376
436
  switch (cleanKey) {
377
437
  case 'copy-uid':
438
+ closeDropdown();
378
439
  handleCopyUid();
379
440
  break;
380
441
  case 'delete':
@@ -385,7 +446,15 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
385
446
  break;
386
447
  }
387
448
  },
388
- [handleCopyUid, handleDelete, handleStepConfiguration, handleCopyPopupUid, extraMenuItems],
449
+ [
450
+ closeDropdown,
451
+ handleCopyUid,
452
+ handleDelete,
453
+ handleStepConfiguration,
454
+ handleCopyPopupUid,
455
+ extraMenuItems,
456
+ isStepMenuItemDisabled,
457
+ ],
389
458
  );
390
459
 
391
460
  // 获取单个模型的可配置flows和steps
@@ -409,6 +478,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
409
478
  if (await shouldHideStepInSettings(targetModel, flow, actionStep)) {
410
479
  return null;
411
480
  }
481
+ const disabledState = await resolveStepDisabledInSettings(targetModel, flow, actionStep as any);
412
482
  let uiMode: any = await resolveUiMode(actionStep.uiMode, (targetModel as any).context);
413
483
  // 检查是否有uiSchema(静态或动态)
414
484
  const hasStepUiSchema = actionStep.uiSchema != null;
@@ -458,6 +528,8 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
458
528
  title: t(stepTitle) || stepKey,
459
529
  modelKey, // 添加模型标识
460
530
  uiMode,
531
+ disabled: disabledState.disabled,
532
+ disabledReason: disabledState.reason,
461
533
  };
462
534
  }),
463
535
  ).then((steps) => steps.filter(Boolean));
@@ -494,9 +566,6 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
494
566
  return result;
495
567
  }, [model, menuLevels, getModelConfigurableFlowsAndSteps]);
496
568
 
497
- const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = useState<FlowInfo[]>([]);
498
- const [isLoading, setIsLoading] = useState(true);
499
-
500
569
  useEffect(() => {
501
570
  const triggerRebuild = () => {
502
571
  setRefreshTick((v) => v + 1);
@@ -603,10 +672,14 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
603
672
  },
604
673
  ...((uiMode as any)?.props || {}),
605
674
  itemKey: (uiMode as any)?.key,
675
+ disabled: !!stepInfo.disabled,
676
+ disabledReason: stepInfo.disabledReason,
677
+ disabledIconColor,
606
678
  };
607
679
  items.push({
608
680
  key: uniqueKey,
609
- label: <MenuLabelItem title={t(stepInfo.title)} uiMode={uiMode} itemProps={itemProps} />,
681
+ label: <MenuLabelItem title={stepInfo.title} uiMode={uiMode} itemProps={itemProps} />,
682
+ disabled: !!stepInfo.disabled,
610
683
  });
611
684
  });
612
685
  if (flow.options.divider === 'bottom') {
@@ -648,7 +721,17 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
648
721
 
649
722
  items.push({
650
723
  key: uniqueKey,
651
- label: t(stepInfo.title),
724
+ label: stepInfo.disabled ? (
725
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
726
+ {stepInfo.title}
727
+ <Tooltip title={stepInfo.disabledReason} placement="right" destroyTooltipOnHide>
728
+ <QuestionCircleOutlined style={{ color: disabledIconColor }} />
729
+ </Tooltip>
730
+ </span>
731
+ ) : (
732
+ stepInfo.title
733
+ ),
734
+ disabled: !!stepInfo.disabled,
652
735
  });
653
736
  });
654
737
  });
@@ -663,7 +746,17 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
663
746
 
664
747
  subMenuChildren.push({
665
748
  key: uniqueKey,
666
- label: t(stepInfo.title),
749
+ label: stepInfo.disabled ? (
750
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
751
+ {stepInfo.title}
752
+ <Tooltip title={stepInfo.disabledReason} placement="right" destroyTooltipOnHide>
753
+ <QuestionCircleOutlined style={{ color: disabledIconColor }} />
754
+ </Tooltip>
755
+ </span>
756
+ ) : (
757
+ stepInfo.title
758
+ ),
759
+ disabled: !!stepInfo.disabled,
667
760
  });
668
761
  });
669
762
  });
@@ -679,7 +772,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
679
772
  }
680
773
 
681
774
  return items;
682
- }, [configurableFlowsAndSteps, flattenSubMenus, t]);
775
+ }, [configurableFlowsAndSteps, disabledIconColor, flattenSubMenus, t]);
683
776
 
684
777
  // 向菜单项添加额外按钮
685
778
  const finalMenuItems = useMemo((): NonNullable<MenuProps['items']> => {