@nocobase/flow-engine 2.1.0-beta.8 → 2.2.0-alpha.1

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 (215) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/FieldModelRenderer.js +2 -2
  4. package/lib/components/FlowModelRenderer.d.ts +3 -1
  5. package/lib/components/FlowModelRenderer.js +12 -6
  6. package/lib/components/FormItem.d.ts +6 -0
  7. package/lib/components/FormItem.js +11 -3
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  10. package/lib/components/dnd/gridDragPlanner.js +607 -19
  11. package/lib/components/dnd/index.d.ts +31 -2
  12. package/lib/components/dnd/index.js +244 -23
  13. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  14. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  15. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
  17. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  18. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  19. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  20. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  21. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  22. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  23. package/lib/components/subModel/AddSubModelButton.js +12 -1
  24. package/lib/components/subModel/LazyDropdown.js +301 -52
  25. package/lib/components/subModel/index.d.ts +1 -0
  26. package/lib/components/subModel/index.js +19 -0
  27. package/lib/components/subModel/utils.d.ts +2 -1
  28. package/lib/components/subModel/utils.js +15 -5
  29. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  30. package/lib/components/variables/VariableHybridInput.js +499 -0
  31. package/lib/components/variables/index.d.ts +2 -0
  32. package/lib/components/variables/index.js +3 -0
  33. package/lib/data-source/index.d.ts +84 -0
  34. package/lib/data-source/index.js +269 -7
  35. package/lib/executor/FlowExecutor.js +6 -3
  36. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  37. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  38. package/lib/flow-registry/index.d.ts +1 -0
  39. package/lib/flow-registry/index.js +3 -1
  40. package/lib/flowContext.d.ts +9 -1
  41. package/lib/flowContext.js +77 -6
  42. package/lib/flowEngine.d.ts +136 -4
  43. package/lib/flowEngine.js +429 -51
  44. package/lib/flowI18n.js +2 -1
  45. package/lib/flowSettings.d.ts +14 -6
  46. package/lib/flowSettings.js +34 -6
  47. package/lib/index.d.ts +2 -0
  48. package/lib/index.js +7 -0
  49. package/lib/lazy-helper.d.ts +14 -0
  50. package/lib/lazy-helper.js +71 -0
  51. package/lib/locale/en-US.json +1 -0
  52. package/lib/locale/index.d.ts +2 -0
  53. package/lib/locale/zh-CN.json +1 -0
  54. package/lib/models/DisplayItemModel.d.ts +1 -1
  55. package/lib/models/EditableItemModel.d.ts +1 -1
  56. package/lib/models/FilterableItemModel.d.ts +1 -1
  57. package/lib/models/flowModel.d.ts +13 -10
  58. package/lib/models/flowModel.js +126 -34
  59. package/lib/provider.js +38 -23
  60. package/lib/reactive/observer.js +46 -16
  61. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  62. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  63. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  64. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  65. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  66. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  67. package/lib/runjs-context/contexts/base.js +464 -29
  68. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  69. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  70. package/lib/runjs-context/setup.js +1 -0
  71. package/lib/runjs-context/snippets/index.js +13 -2
  72. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  74. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  75. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/loadedPageCache.d.ts +24 -0
  82. package/lib/utils/loadedPageCache.js +139 -0
  83. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  84. package/lib/utils/parsePathnameToViewParams.js +28 -4
  85. package/lib/utils/randomId.d.ts +39 -0
  86. package/lib/utils/randomId.js +45 -0
  87. package/lib/utils/runjsTemplateCompat.js +1 -1
  88. package/lib/utils/runjsValue.js +41 -11
  89. package/lib/utils/schema-utils.d.ts +7 -1
  90. package/lib/utils/schema-utils.js +19 -0
  91. package/lib/views/FlowView.d.ts +7 -1
  92. package/lib/views/FlowView.js +11 -1
  93. package/lib/views/PageComponent.js +8 -6
  94. package/lib/views/ViewNavigation.d.ts +12 -2
  95. package/lib/views/ViewNavigation.js +28 -9
  96. package/lib/views/createViewMeta.js +114 -50
  97. package/lib/views/inheritLayoutContext.d.ts +10 -0
  98. package/lib/views/inheritLayoutContext.js +50 -0
  99. package/lib/views/runViewBeforeClose.d.ts +10 -0
  100. package/lib/views/runViewBeforeClose.js +45 -0
  101. package/lib/views/useDialog.d.ts +2 -1
  102. package/lib/views/useDialog.js +12 -3
  103. package/lib/views/useDrawer.d.ts +2 -1
  104. package/lib/views/useDrawer.js +12 -3
  105. package/lib/views/usePage.d.ts +5 -11
  106. package/lib/views/usePage.js +304 -144
  107. package/package.json +5 -4
  108. package/src/FlowContextProvider.tsx +9 -1
  109. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  110. package/src/__tests__/flow-engine.test.ts +166 -0
  111. package/src/__tests__/flowContext.test.ts +105 -1
  112. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  113. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +21 -0
  120. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  121. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  122. package/src/__tests__/runjsLocales.test.ts +6 -5
  123. package/src/__tests__/runjsSnippets.test.ts +21 -0
  124. package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
  125. package/src/components/FieldModelRenderer.tsx +2 -1
  126. package/src/components/FlowModelRenderer.tsx +18 -6
  127. package/src/components/FormItem.tsx +7 -1
  128. package/src/components/MobilePopup.tsx +4 -2
  129. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  130. package/src/components/__tests__/FormItem.test.tsx +25 -0
  131. package/src/components/__tests__/dnd.test.ts +44 -0
  132. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  133. package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
  134. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  135. package/src/components/dnd/gridDragPlanner.ts +750 -17
  136. package/src/components/dnd/index.tsx +305 -28
  137. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  138. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
  139. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +16 -2
  145. package/src/components/subModel/LazyDropdown.tsx +341 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +13 -2
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +69 -2
  154. package/src/data-source/index.ts +332 -8
  155. package/src/executor/FlowExecutor.ts +6 -3
  156. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +85 -6
  161. package/src/flowEngine.ts +484 -45
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  172. package/src/models/__tests__/flowModel.test.ts +65 -37
  173. package/src/models/flowModel.tsx +184 -65
  174. package/src/provider.tsx +41 -25
  175. package/src/reactive/__tests__/observer.test.tsx +82 -0
  176. package/src/reactive/observer.tsx +87 -25
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  179. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  180. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  181. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  182. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  183. package/src/runjs-context/contexts/base.ts +467 -31
  184. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  185. package/src/runjs-context/setup.ts +1 -0
  186. package/src/runjs-context/snippets/index.ts +12 -1
  187. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  188. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  189. package/src/types.ts +62 -0
  190. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  191. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  192. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  193. package/src/utils/__tests__/utils.test.ts +62 -0
  194. package/src/utils/createCollectionContextMeta.ts +6 -2
  195. package/src/utils/index.ts +5 -1
  196. package/src/utils/loadedPageCache.ts +147 -0
  197. package/src/utils/parsePathnameToViewParams.ts +45 -5
  198. package/src/utils/randomId.ts +48 -0
  199. package/src/utils/runjsTemplateCompat.ts +1 -1
  200. package/src/utils/runjsValue.ts +50 -11
  201. package/src/utils/schema-utils.ts +30 -1
  202. package/src/views/FlowView.tsx +22 -2
  203. package/src/views/PageComponent.tsx +7 -4
  204. package/src/views/ViewNavigation.ts +46 -9
  205. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  206. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  207. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  208. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  209. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
  210. package/src/views/createViewMeta.ts +106 -34
  211. package/src/views/inheritLayoutContext.ts +26 -0
  212. package/src/views/runViewBeforeClose.ts +19 -0
  213. package/src/views/useDialog.tsx +13 -3
  214. package/src/views/useDrawer.tsx +13 -3
  215. package/src/views/usePage.tsx +367 -180
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { ExclamationCircleOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
11
+ import { css } from '@emotion/css';
11
12
  import type { DropdownProps, MenuProps } from 'antd';
12
13
  import { App, Dropdown, Modal, Tooltip, theme } from 'antd';
13
14
  import React, { startTransition, useCallback, useEffect, useMemo, useState, FC } from 'react';
@@ -26,6 +27,26 @@ import { useNiceDropdownMaxHeight } from '../../../../hooks';
26
27
  import { SwitchWithTitle } from '../component/SwitchWithTitle';
27
28
  import { SelectWithTitle } from '../component/SelectWithTitle';
28
29
  import type { FlowSettingsContext } from '../../../../flowContext';
30
+
31
+ const findExtraMenuItemByKey = (
32
+ items: FlowModelExtraMenuItem[],
33
+ targetKey: string,
34
+ ): FlowModelExtraMenuItem | undefined => {
35
+ for (const item of items) {
36
+ const itemKey = String(item?.key ?? '');
37
+ if (itemKey === targetKey) {
38
+ return item;
39
+ }
40
+ if (item.children?.length) {
41
+ const matched = findExtraMenuItemByKey(item.children, targetKey);
42
+ if (matched) {
43
+ return matched;
44
+ }
45
+ }
46
+ }
47
+ return undefined;
48
+ };
49
+
29
50
  // Type definitions for better type safety
30
51
  interface StepInfo {
31
52
  stepKey: string;
@@ -188,15 +209,51 @@ interface DefaultSettingsIconProps {
188
209
  showCopyUidButton?: boolean;
189
210
  menuLevels?: number; // Menu levels: 1=current model only (default), 2=include sub-models
190
211
  flattenSubMenus?: boolean; // Whether to flatten sub-menus: false=group by model (default), true=flatten all
212
+ onDropdownVisibleChange?: (open: boolean) => void;
213
+ getPopupContainer?: DropdownProps['getPopupContainer'];
191
214
  [key: string]: any; // Allow additional props
192
215
  }
193
216
 
217
+ const TOOLBAR_ICONS_SELECTOR = '.nb-toolbar-container-icons';
218
+ const TOOLBAR_CONTAINER_SELECTOR = '.nb-toolbar-container';
219
+ const TOOLBAR_DROPDOWN_OVERLAY_CLASS = css`
220
+ width: max-content;
221
+ min-width: max-content;
222
+
223
+ .ant-dropdown-menu {
224
+ width: max-content;
225
+ min-width: max-content;
226
+ }
227
+ `;
228
+
229
+ const getToolbarPopupContainer = (triggerNode?: HTMLElement | null) => {
230
+ if (!triggerNode) {
231
+ return null;
232
+ }
233
+
234
+ return (
235
+ (triggerNode.closest(TOOLBAR_ICONS_SELECTOR) as HTMLElement | null) ||
236
+ (triggerNode.closest(TOOLBAR_CONTAINER_SELECTOR) as HTMLElement | null)
237
+ );
238
+ };
239
+
240
+ const removeExtraMenuItemClickHandlers = (item: FlowModelExtraMenuItem): FlowModelExtraMenuItem => {
241
+ const { onClick: _onClick, children, ...rest } = item;
242
+
243
+ return {
244
+ ...rest,
245
+ children: children?.length ? children.map(removeExtraMenuItemClickHandlers) : undefined,
246
+ };
247
+ };
248
+
194
249
  export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
195
250
  model,
196
251
  showDeleteButton = true,
197
252
  showCopyUidButton = true,
198
253
  menuLevels = 1, // 默认一级菜单
199
254
  flattenSubMenus = true,
255
+ onDropdownVisibleChange,
256
+ getPopupContainer,
200
257
  }) => {
201
258
  const { message } = App.useApp();
202
259
  const t = useMemo(() => getT(model), [model]);
@@ -206,43 +263,80 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
206
263
  // 当模型发生子模型替换/增删等变化时,强制刷新菜单数据
207
264
  const [refreshTick, setRefreshTick] = useState(0);
208
265
  const [extraMenuItems, setExtraMenuItems] = useState<FlowModelExtraMenuItem[]>([]);
266
+ const [extraMenuItemsLoaded, setExtraMenuItemsLoaded] = useState(false);
209
267
  const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = useState<FlowInfo[]>([]);
210
268
  const [isLoading, setIsLoading] = useState(true);
269
+ const commonExtras = useMemo(
270
+ () => extraMenuItems.filter((it) => it.group === 'common-actions').sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)),
271
+ [extraMenuItems],
272
+ );
273
+ const hasCommonActions = showCopyUidButton || showDeleteButton || commonExtras.length > 0;
274
+ const shouldDeferConfigLoading = flattenSubMenus && menuLevels > 1 && hasCommonActions;
275
+ const shouldWaitForCommonActionProbe =
276
+ flattenSubMenus && menuLevels > 1 && !showCopyUidButton && !showDeleteButton && !extraMenuItemsLoaded;
277
+ const canRenderIcon = hasCommonActions || (!isLoading && configurableFlowsAndSteps.length > 0);
211
278
  const closeDropdown = useCallback(() => {
212
279
  setVisible(false);
213
- }, []);
214
- const handleOpenChange: DropdownProps['onOpenChange'] = useCallback((nextOpen: boolean, info) => {
215
- if (info.source === 'trigger' || nextOpen) {
216
- // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
217
- startTransition(() => {
218
- setVisible(nextOpen);
219
- });
220
- }
221
- }, []);
280
+ onDropdownVisibleChange?.(false);
281
+ }, [onDropdownVisibleChange]);
282
+ const resolvePopupContainer = useCallback<NonNullable<DropdownProps['getPopupContainer']>>(
283
+ (triggerNode) => {
284
+ // 工具栏自身容器必须优先,保证鼠标从 icon 移到菜单时仍处于同一 hover 树。
285
+ // 弹窗场景的裁剪问题由 useFloatToolbarPortal 负责把 toolbar 挂到正确的 popup host。
286
+ return (
287
+ getToolbarPopupContainer(triggerNode) ||
288
+ getPopupContainer?.(triggerNode) ||
289
+ triggerNode?.parentElement ||
290
+ document.body
291
+ );
292
+ },
293
+ [getPopupContainer],
294
+ );
295
+ const handleOpenChange: DropdownProps['onOpenChange'] = useCallback(
296
+ (nextOpen: boolean, info) => {
297
+ if (info.source === 'trigger' || nextOpen) {
298
+ // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
299
+ startTransition(() => {
300
+ setVisible(nextOpen);
301
+ });
302
+ onDropdownVisibleChange?.(nextOpen);
303
+ }
304
+ },
305
+ [onDropdownVisibleChange],
306
+ );
307
+ useEffect(() => {
308
+ return () => {
309
+ onDropdownVisibleChange?.(false);
310
+ };
311
+ }, [onDropdownVisibleChange]);
222
312
  const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]);
223
313
  useEffect(() => {
224
314
  let mounted = true;
225
315
  const loadExtras = async () => {
226
- const allExtras: FlowModelExtraMenuItem[] = [];
227
- const modelsToProcess: Array<{ model: FlowModel; modelKey?: string }> = [];
228
- walkSubModels(model, { maxDepth: menuLevels, arrayLimit: 50, mode: 'stack' }, (targetModel, { modelKey }) => {
229
- modelsToProcess.push({ model: targetModel, modelKey });
230
- });
316
+ setExtraMenuItemsLoaded(false);
317
+ try {
318
+ const allExtras: FlowModelExtraMenuItem[] = [];
319
+ const modelsToProcess: Array<{ model: FlowModel; modelKey?: string }> = [];
320
+ walkSubModels(model, { maxDepth: menuLevels, arrayLimit: 50, mode: 'stack' }, (targetModel, { modelKey }) => {
321
+ modelsToProcess.push({ model: targetModel, modelKey });
322
+ });
231
323
 
232
- for (const { model: targetModel, modelKey } of modelsToProcess) {
233
- const Cls = targetModel.constructor as typeof FlowModel;
234
- const extras = await Cls.getExtraMenuItems?.(targetModel, t);
235
- if (extras?.length) {
236
- allExtras.push(
237
- ...extras.map((item) => ({
238
- ...item,
239
- key: modelKey ? `${modelKey}:${item.key}` : item.key,
240
- })),
241
- );
324
+ for (const { model: targetModel, modelKey } of modelsToProcess) {
325
+ const Cls = targetModel.constructor as typeof FlowModel;
326
+ const extras = await Cls.getExtraMenuItems?.(targetModel, t);
327
+ if (extras?.length) {
328
+ allExtras.push(
329
+ ...extras.map((item) => ({
330
+ ...item,
331
+ key: modelKey ? `${modelKey}:${item.key}` : item.key,
332
+ })),
333
+ );
334
+ }
242
335
  }
243
- }
244
336
 
245
- if (mounted) {
337
+ if (!mounted) {
338
+ return;
339
+ }
246
340
  const seen = new Set<string>();
247
341
  const dedupedExtras = allExtras.filter((item) => {
248
342
  if (seen.has(`${item.key}`)) {
@@ -252,16 +346,22 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
252
346
  return true;
253
347
  });
254
348
  setExtraMenuItems(dedupedExtras);
349
+ } catch (error) {
350
+ console.error('Failed to load extra menu items:', error);
351
+ if (mounted) {
352
+ setExtraMenuItems([]);
353
+ }
354
+ } finally {
355
+ if (mounted) {
356
+ setExtraMenuItemsLoaded(true);
357
+ }
255
358
  }
256
359
  };
257
- // 避免 effect 触发 setState 导致循环:仅在 visible 打开时加载一次,关闭后仍保留结果
258
- if (visible) {
259
- loadExtras();
260
- }
360
+ loadExtras();
261
361
  return () => {
262
362
  mounted = false;
263
363
  };
264
- }, [model, menuLevels, t, refreshTick, visible]);
364
+ }, [model, menuLevels, t, refreshTick]);
265
365
 
266
366
  // 统一的复制 UID 方法
267
367
  const copyUidToClipboard = useCallback(
@@ -422,7 +522,11 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
422
522
  return;
423
523
  }
424
524
 
425
- const extra = extraMenuItems.find((it) => it?.key === originalKey || it?.key === cleanKey);
525
+ const extra =
526
+ findExtraMenuItemByKey(extraMenuItems, originalKey) || findExtraMenuItemByKey(extraMenuItems, cleanKey);
527
+ if (extra?.disabled) {
528
+ return;
529
+ }
426
530
  if (extra?.onClick) {
427
531
  closeDropdown();
428
532
  extra.onClick();
@@ -548,7 +652,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
548
652
  return [];
549
653
  }
550
654
  },
551
- [],
655
+ [t],
552
656
  );
553
657
 
554
658
  // 获取可配置的flows和steps
@@ -591,21 +695,50 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
591
695
  }, [model, menuLevels, refreshTick]);
592
696
 
593
697
  useEffect(() => {
698
+ let mounted = true;
594
699
  const loadConfigurableFlowsAndSteps = async () => {
595
700
  setIsLoading(true);
701
+ if (shouldDeferConfigLoading) {
702
+ setConfigurableFlowsAndSteps([]);
703
+ }
596
704
  try {
597
705
  const flows = await getConfigurableFlowsAndSteps();
598
- setConfigurableFlowsAndSteps(flows);
706
+ if (mounted) {
707
+ setConfigurableFlowsAndSteps(flows);
708
+ }
599
709
  } catch (error) {
600
710
  console.error('Failed to load configurable flows and steps:', error);
601
- setConfigurableFlowsAndSteps([]);
711
+ if (mounted) {
712
+ setConfigurableFlowsAndSteps([]);
713
+ }
602
714
  } finally {
603
- setIsLoading(false);
715
+ if (mounted) {
716
+ setIsLoading(false);
717
+ }
604
718
  }
605
719
  };
606
720
 
721
+ if (shouldWaitForCommonActionProbe) {
722
+ setConfigurableFlowsAndSteps([]);
723
+ setIsLoading(false);
724
+ return () => {
725
+ mounted = false;
726
+ };
727
+ }
728
+
729
+ if (!visible && shouldDeferConfigLoading) {
730
+ setConfigurableFlowsAndSteps([]);
731
+ setIsLoading(false);
732
+ return () => {
733
+ mounted = false;
734
+ };
735
+ }
736
+
607
737
  loadConfigurableFlowsAndSteps();
608
- }, [getConfigurableFlowsAndSteps, refreshTick]);
738
+ return () => {
739
+ mounted = false;
740
+ };
741
+ }, [getConfigurableFlowsAndSteps, refreshTick, shouldDeferConfigLoading, shouldWaitForCommonActionProbe, visible]);
609
742
 
610
743
  // 构建菜单项,包含错误处理和记忆化
611
744
  const menuItems = useMemo((): NonNullable<MenuProps['items']> => {
@@ -772,16 +905,12 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
772
905
  }
773
906
 
774
907
  return items;
775
- }, [configurableFlowsAndSteps, disabledIconColor, flattenSubMenus, t]);
908
+ }, [configurableFlowsAndSteps, disabledIconColor, flattenSubMenus, message, model, t]);
776
909
 
777
910
  // 向菜单项添加额外按钮
778
911
  const finalMenuItems = useMemo((): NonNullable<MenuProps['items']> => {
779
912
  const items = [...menuItems];
780
913
 
781
- const commonExtras = extraMenuItems
782
- .filter((it) => it.group === 'common-actions')
783
- .sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
784
-
785
914
  if (showCopyUidButton || showDeleteButton || commonExtras.length > 0) {
786
915
  items.push({
787
916
  type: 'divider',
@@ -795,7 +924,8 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
795
924
  // });
796
925
 
797
926
  if (commonExtras.length > 0) {
798
- items.push(...(commonExtras as MenuProps['items']));
927
+ // Antd Menu 会同时触发 item.onClick menu.onClick,这里统一交给 handleMenuClick 执行。
928
+ items.push(...(commonExtras.map(removeExtraMenuItemClickHandlers) as MenuProps['items']));
799
929
  }
800
930
 
801
931
  // 添加复制uid按钮
@@ -816,12 +946,9 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
816
946
  }
817
947
 
818
948
  return items;
819
- }, [menuItems, showCopyUidButton, showDeleteButton, model.uid, model.destroy, t, extraMenuItems]);
820
-
821
- // 如果正在加载或没有可配置的flows且不显示删除按钮和复制UID按钮,不显示菜单
822
- const hasExtras = extraMenuItems.some((it) => it.group === 'common-actions');
949
+ }, [menuItems, showCopyUidButton, showDeleteButton, commonExtras, model.uid, model.destroy, t]);
823
950
 
824
- if (isLoading || (configurableFlowsAndSteps.length === 0 && !showDeleteButton && !showCopyUidButton && !hasExtras)) {
951
+ if (!canRenderIcon) {
825
952
  return null;
826
953
  }
827
954
 
@@ -833,6 +960,9 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
833
960
 
834
961
  return (
835
962
  <Dropdown
963
+ getPopupContainer={resolvePopupContainer}
964
+ overlayClassName={TOOLBAR_DROPDOWN_OVERLAY_CLASS}
965
+ overlayStyle={{ width: 'max-content', minWidth: 'max-content' }}
836
966
  onOpenChange={handleOpenChange}
837
967
  open={visible}
838
968
  menu={{