@nocobase/flow-engine 2.1.0-beta.2 → 2.1.0-beta.20

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 (126) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/JSRunner.d.ts +10 -1
  4. package/lib/JSRunner.js +50 -5
  5. package/lib/ViewScopedFlowEngine.js +5 -1
  6. package/lib/components/FlowModelRenderer.d.ts +1 -1
  7. package/lib/components/FlowModelRenderer.js +10 -6
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.js +6 -2
  10. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  11. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +48 -9
  12. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +19 -43
  13. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +339 -295
  14. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  15. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  16. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +272 -0
  17. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  18. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +247 -0
  19. package/lib/components/subModel/AddSubModelButton.js +27 -1
  20. package/lib/components/subModel/utils.js +2 -2
  21. package/lib/data-source/index.js +6 -0
  22. package/lib/executor/FlowExecutor.js +31 -8
  23. package/lib/flowContext.js +31 -1
  24. package/lib/flowEngine.d.ts +151 -1
  25. package/lib/flowEngine.js +389 -15
  26. package/lib/flowSettings.d.ts +14 -6
  27. package/lib/flowSettings.js +34 -6
  28. package/lib/lazy-helper.d.ts +14 -0
  29. package/lib/lazy-helper.js +71 -0
  30. package/lib/locale/en-US.json +1 -0
  31. package/lib/locale/index.d.ts +2 -0
  32. package/lib/locale/zh-CN.json +1 -0
  33. package/lib/models/flowModel.d.ts +2 -1
  34. package/lib/models/flowModel.js +28 -9
  35. package/lib/reactive/observer.js +46 -16
  36. package/lib/runjs-context/registry.d.ts +1 -1
  37. package/lib/runjs-context/setup.js +20 -12
  38. package/lib/runjs-context/snippets/index.js +13 -2
  39. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  40. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  41. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  42. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  43. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  44. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  45. package/lib/types.d.ts +47 -1
  46. package/lib/utils/index.d.ts +2 -2
  47. package/lib/utils/index.js +4 -0
  48. package/lib/utils/parsePathnameToViewParams.js +1 -1
  49. package/lib/utils/runjsTemplateCompat.js +1 -1
  50. package/lib/utils/runjsValue.js +41 -11
  51. package/lib/utils/schema-utils.d.ts +7 -1
  52. package/lib/utils/schema-utils.js +19 -0
  53. package/lib/views/FlowView.d.ts +7 -1
  54. package/lib/views/runViewBeforeClose.d.ts +10 -0
  55. package/lib/views/runViewBeforeClose.js +45 -0
  56. package/lib/views/useDialog.d.ts +2 -1
  57. package/lib/views/useDialog.js +20 -3
  58. package/lib/views/useDrawer.d.ts +2 -1
  59. package/lib/views/useDrawer.js +20 -3
  60. package/lib/views/usePage.d.ts +2 -1
  61. package/lib/views/usePage.js +10 -3
  62. package/package.json +6 -5
  63. package/src/JSRunner.ts +68 -4
  64. package/src/ViewScopedFlowEngine.ts +4 -0
  65. package/src/__tests__/JSRunner.test.ts +27 -1
  66. package/src/__tests__/flow-engine.test.ts +166 -0
  67. package/src/__tests__/flowContext.test.ts +65 -1
  68. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  69. package/src/__tests__/flowSettings.test.ts +94 -15
  70. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  71. package/src/__tests__/runjsContext.test.ts +16 -0
  72. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  73. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  74. package/src/__tests__/runjsSnippets.test.ts +21 -0
  75. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  76. package/src/components/FlowModelRenderer.tsx +12 -6
  77. package/src/components/MobilePopup.tsx +4 -2
  78. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  79. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  80. package/src/components/__tests__/gridDragPlanner.test.ts +88 -0
  81. package/src/components/dnd/gridDragPlanner.ts +8 -2
  82. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +63 -9
  83. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +468 -440
  84. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  85. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +95 -0
  86. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +609 -0
  87. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +358 -0
  88. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +281 -0
  89. package/src/components/subModel/AddSubModelButton.tsx +32 -2
  90. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
  91. package/src/components/subModel/utils.ts +1 -1
  92. package/src/data-source/index.ts +6 -0
  93. package/src/executor/FlowExecutor.ts +34 -9
  94. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  95. package/src/flowContext.ts +35 -3
  96. package/src/flowEngine.ts +445 -11
  97. package/src/flowSettings.ts +40 -6
  98. package/src/lazy-helper.tsx +57 -0
  99. package/src/locale/en-US.json +1 -0
  100. package/src/locale/zh-CN.json +1 -0
  101. package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
  102. package/src/models/flowModel.tsx +31 -10
  103. package/src/reactive/__tests__/observer.test.tsx +82 -0
  104. package/src/reactive/observer.tsx +87 -25
  105. package/src/runjs-context/registry.ts +1 -1
  106. package/src/runjs-context/setup.ts +22 -12
  107. package/src/runjs-context/snippets/index.ts +12 -1
  108. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  109. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  110. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  111. package/src/types.ts +60 -0
  112. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  113. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  114. package/src/utils/__tests__/utils.test.ts +62 -0
  115. package/src/utils/index.ts +2 -1
  116. package/src/utils/parsePathnameToViewParams.ts +2 -2
  117. package/src/utils/runjsTemplateCompat.ts +1 -1
  118. package/src/utils/runjsValue.ts +50 -11
  119. package/src/utils/schema-utils.ts +30 -1
  120. package/src/views/FlowView.tsx +11 -1
  121. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  122. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  123. package/src/views/runViewBeforeClose.ts +19 -0
  124. package/src/views/useDialog.tsx +25 -3
  125. package/src/views/useDrawer.tsx +25 -3
  126. package/src/views/usePage.tsx +12 -3
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
11
+ import { createPortal } from 'react-dom';
11
12
  import { Alert, Space } from 'antd';
12
13
  import { css } from '@emotion/css';
13
14
  import { FlowModel } from '../../../../models';
@@ -18,16 +19,264 @@ import { FlowEngine } from '../../../../flowEngine';
18
19
  import { getT } from '../../../../utils';
19
20
  import { useFlowContext } from '../../../..';
20
21
  import { observer } from '../../../../reactive';
22
+ import {
23
+ omitToolbarPortalInsetStyle,
24
+ ToolbarPortalRect,
25
+ ToolbarPortalRenderSnapshot,
26
+ useFloatToolbarPortal,
27
+ } from './useFloatToolbarPortal';
28
+ import { useFloatToolbarVisibility } from './useFloatToolbarVisibility';
21
29
 
22
- // 检测DOM中直接子元素是否包含button元素的辅助函数
30
+ const TOOLBAR_Z_INDEX = 999;
31
+
32
+ type ToolbarPosition = 'inside' | 'above' | 'below';
33
+
34
+ interface BaseFloatContextMenuProps {
35
+ children?: React.ReactNode;
36
+ enabled?: boolean;
37
+ showDeleteButton?: boolean;
38
+ showCopyUidButton?: boolean;
39
+ containerStyle?: React.CSSProperties;
40
+ /** 自定义工具栏样式,`top/left/right/bottom` 会作为 portal overlay 的 inset 使用。 */
41
+ toolbarStyle?: React.CSSProperties;
42
+ className?: string;
43
+ /**
44
+ * @default true
45
+ */
46
+ showBorder?: boolean;
47
+ /**
48
+ * @default true
49
+ */
50
+ showBackground?: boolean;
51
+ /**
52
+ * @default false
53
+ */
54
+ showTitle?: boolean;
55
+ /**
56
+ * @default false
57
+ */
58
+ showDragHandle?: boolean;
59
+ /**
60
+ * Settings menu levels: 1=current model only (default), 2=include sub-models
61
+ */
62
+ settingsMenuLevel?: number;
63
+ /**
64
+ * Extra toolbar items to add to this context menu instance
65
+ */
66
+ extraToolbarItems?: ToolbarItemConfig[];
67
+ /**
68
+ * @default 'inside'
69
+ */
70
+ toolbarPosition?: ToolbarPosition;
71
+ }
72
+
73
+ const getFloatMenuInstanceId = (model?: FlowModel | null) => {
74
+ if (!model) {
75
+ return '';
76
+ }
77
+
78
+ const forkId = (model as any)?.isFork ? (model as any)?.forkId : undefined;
79
+ return forkId == null || forkId === '' ? String(model.uid || '') : `${String(model.uid || '')}::${String(forkId)}`;
80
+ };
81
+
82
+ const hostContainerStyles = css`
83
+ position: relative;
84
+
85
+ &.has-button-child {
86
+ display: inline-block;
87
+ }
88
+ `;
89
+
90
+ const toolbarPositionClassNames: Record<ToolbarPosition, string> = {
91
+ inside: 'nb-toolbar-position-inside',
92
+ above: 'nb-toolbar-position-above',
93
+ below: 'nb-toolbar-position-below',
94
+ };
95
+
96
+ const toolbarContainerStyles = ({
97
+ showBackground,
98
+ showBorder,
99
+ ctx,
100
+ }: {
101
+ showBackground: boolean;
102
+ showBorder: boolean;
103
+ ctx: any;
104
+ }) => css`
105
+ z-index: ${TOOLBAR_Z_INDEX};
106
+ opacity: 0;
107
+ pointer-events: none;
108
+ overflow: visible;
109
+ transition: opacity 0.12s ease;
110
+ background: ${showBackground ? 'var(--colorBgSettingsHover)' : 'transparent'};
111
+ border: ${showBorder ? '2px solid var(--colorBorderSettingsHover)' : 'none'};
112
+ border-radius: ${ctx.themeToken.borderRadiusLG}px;
113
+
114
+ &.nb-toolbar-visible {
115
+ opacity: 1;
116
+ transition-delay: 0.1s;
117
+ }
118
+
119
+ &.nb-toolbar-portal {
120
+ top: 0;
121
+ left: 0;
122
+ }
123
+
124
+ &.nb-toolbar-portal-fixed {
125
+ position: fixed;
126
+ }
127
+
128
+ &.nb-toolbar-portal-absolute {
129
+ position: absolute;
130
+ }
131
+
132
+ &.nb-in-template {
133
+ background: var(--colorTemplateBgSettingsHover);
134
+ }
135
+
136
+ > .nb-toolbar-container-title {
137
+ pointer-events: none;
138
+ position: absolute;
139
+ top: 2px;
140
+ left: 2px;
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 4px;
144
+ height: 16px;
145
+ padding: 0;
146
+ font-size: 12px;
147
+ line-height: 16px;
148
+ border-bottom-right-radius: 2px;
149
+ border-radius: 2px;
150
+
151
+ .title-tag {
152
+ display: inline-flex;
153
+ padding: 0 3px;
154
+ border-radius: 2px;
155
+ background: var(--colorSettings);
156
+ color: #fff;
157
+ }
158
+ }
159
+
160
+ > .nb-toolbar-container-icons {
161
+ display: none;
162
+ position: absolute;
163
+ right: 2px;
164
+ line-height: 16px;
165
+ pointer-events: all;
166
+
167
+ &.nb-toolbar-position-inside {
168
+ top: 2px;
169
+ }
170
+
171
+ &.nb-toolbar-position-above {
172
+ top: 0;
173
+ transform: translateY(-100%);
174
+ padding-bottom: 0;
175
+ margin-bottom: -2px;
176
+ }
177
+
178
+ &.nb-toolbar-position-below {
179
+ top: 0;
180
+ transform: translateY(100%);
181
+ padding-top: 2px;
182
+ margin-top: -2px;
183
+ }
184
+
185
+ .ant-space-item {
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ width: 16px;
190
+ height: 16px;
191
+ padding: 2px;
192
+ line-height: 16px;
193
+ background-color: var(--colorSettings);
194
+ color: #fff;
195
+ }
196
+ }
197
+
198
+ &.nb-toolbar-visible > .nb-toolbar-container-icons {
199
+ display: block;
200
+ }
201
+
202
+ > .resize-handle {
203
+ position: absolute;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ pointer-events: all;
208
+ opacity: 0.6;
209
+ border-radius: 4px;
210
+ background: var(--colorSettings);
211
+
212
+ &:hover {
213
+ opacity: 0.9;
214
+ background: var(--colorSettingsHover, var(--colorSettings));
215
+ }
216
+
217
+ &::before {
218
+ content: '';
219
+ position: absolute;
220
+ border-radius: 50%;
221
+ background: rgba(255, 255, 255, 0.9);
222
+ }
223
+
224
+ &::after {
225
+ content: '';
226
+ position: absolute;
227
+ border-radius: 50%;
228
+ background: rgba(255, 255, 255, 0.9);
229
+ }
230
+ }
231
+
232
+ > .resize-handle-left {
233
+ top: 50%;
234
+ left: -4px;
235
+ width: 6px;
236
+ height: 20px;
237
+ cursor: ew-resize;
238
+ transform: translateY(-50%);
239
+
240
+ &::before {
241
+ top: 6px;
242
+ left: 50%;
243
+ width: 2px;
244
+ height: 2px;
245
+ transform: translateX(-50%);
246
+ box-shadow:
247
+ 0 4px 0 rgba(255, 255, 255, 0.9),
248
+ 0 8px 0 rgba(255, 255, 255, 0.9);
249
+ }
250
+ }
251
+
252
+ > .resize-handle-right {
253
+ top: 50%;
254
+ right: -4px;
255
+ width: 6px;
256
+ height: 20px;
257
+ cursor: ew-resize;
258
+ transform: translateY(-50%);
259
+
260
+ &::before {
261
+ top: 6px;
262
+ left: 50%;
263
+ width: 2px;
264
+ height: 2px;
265
+ transform: translateX(-50%);
266
+ box-shadow:
267
+ 0 4px 0 rgba(255, 255, 255, 0.9),
268
+ 0 8px 0 rgba(255, 255, 255, 0.9);
269
+ }
270
+ }
271
+ `;
272
+
273
+ // 检测直接子节点里是否有按钮,保留原来的 inline-block 兼容行为。
23
274
  const detectButtonInDOM = (container: HTMLElement): boolean => {
24
275
  if (!container) return false;
25
276
 
26
- // 只检测直接子元素中的button
27
277
  const directChildren = container.children;
28
278
  for (let i = 0; i < directChildren.length; i++) {
29
279
  const child = directChildren[i];
30
- // 检查是否是button元素或具有button特征的元素
31
280
  if (child.tagName === 'BUTTON' || child.getAttribute('role') === 'button' || child.classList.contains('ant-btn')) {
32
281
  return true;
33
282
  }
@@ -36,436 +285,184 @@ const detectButtonInDOM = (container: HTMLElement): boolean => {
36
285
  return false;
37
286
  };
38
287
 
39
- // 渲染工具栏项目的辅助函数
288
+ // 渲染工具栏项目,并让设置菜单与工具栏共享同一个 popup 容器。
40
289
  const renderToolbarItems = (
41
290
  model: FlowModel,
291
+ modelInstanceId: string,
42
292
  showDeleteButton: boolean,
43
293
  showCopyUidButton: boolean,
44
294
  flowEngine: FlowEngine,
45
295
  settingsMenuLevel?: number,
46
296
  extraToolbarItems?: ToolbarItemConfig[],
297
+ onSettingsMenuOpenChange?: (open: boolean) => void,
298
+ getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement,
47
299
  ) => {
48
300
  const toolbarItems = flowEngine?.flowSettings?.getToolbarItems?.() || [];
49
-
50
- // 合并额外的工具栏项目
51
301
  const allToolbarItems = [...toolbarItems, ...(extraToolbarItems || [])];
52
302
 
53
- // 按 sort 字段排序
54
303
  allToolbarItems.sort((a, b) => (a.sort || 0) - (b.sort || 0)).reverse();
55
304
 
56
305
  return allToolbarItems
57
306
  .filter((itemConfig: ToolbarItemConfig) => {
58
- // 检查项目是否应该显示
59
307
  return itemConfig.visible ? itemConfig.visible(model) : true;
60
308
  })
61
309
  .map((itemConfig: ToolbarItemConfig) => {
62
- // 渲染项目组件
63
310
  const ItemComponent = itemConfig.component;
64
311
 
65
- // 对于默认设置项目,传递额外的 props
66
312
  if (itemConfig.key === 'settings-menu') {
67
313
  return (
68
314
  <ItemComponent
69
315
  key={itemConfig.key}
70
316
  model={model}
71
- id={model.uid} // 用于拖拽的 id
317
+ id={modelInstanceId}
72
318
  showDeleteButton={showDeleteButton}
73
319
  showCopyUidButton={showCopyUidButton}
74
320
  menuLevels={settingsMenuLevel}
321
+ onDropdownVisibleChange={onSettingsMenuOpenChange}
322
+ getPopupContainer={getPopupContainer}
75
323
  />
76
324
  );
77
325
  }
78
326
 
79
- // 其他项目只传递 model
80
327
  return <ItemComponent key={itemConfig.key} model={model} />;
81
328
  });
82
329
  };
83
330
 
84
- // Width in pixels per toolbar item (icon width + spacing)
85
- const TOOLBAR_ITEM_WIDTH = 19;
86
-
87
- const toolbarPositionToCSS = {
88
- inside: `
89
- top: 2px;
90
- `,
91
- above: `
92
- top: 0px;
93
- transform: translateY(-100%);
94
- padding-bottom: 0px;
95
- margin-bottom: -2px;
96
- `,
97
- below: `
98
- top: 0px;
99
- transform: translateY(100%);
100
- padding-top: 2px;
101
- margin-top: -2px;
102
- `,
103
- };
104
-
105
- // 使用与 NocoBase 一致的悬浮工具栏样式
106
- const floatContainerStyles = ({ showBackground, showBorder, ctx, toolbarPosition = 'inside', toolbarCount }) => css`
107
- position: relative;
108
-
109
- /* 当检测到button时使用inline-block */
110
- &.has-button-child {
111
- display: inline-block;
112
- }
113
-
114
- /* 正常的hover行为 - 添加延迟显示 */
115
- &:hover > .nb-toolbar-container {
116
- opacity: 1;
117
- transition-delay: 0.1s;
118
-
119
- .nb-toolbar-container-icons {
120
- display: block;
121
- }
122
- }
123
-
124
- /* 当有.hide-parent-menu类时隐藏菜单 */
125
- &.hide-parent-menu > .nb-toolbar-container {
126
- opacity: 0 !important;
127
- }
128
-
129
- > .nb-toolbar-container {
130
- position: absolute;
131
- top: 0;
132
- bottom: 0;
133
- left: 0;
134
- right: 0;
135
- z-index: 999;
136
- opacity: 0;
137
- background: ${showBackground ? 'var(--colorBgSettingsHover)' : ''};
138
- border: ${showBorder ? '2px solid var(--colorBorderSettingsHover)' : ''};
139
- border-radius: ${ctx.themeToken.borderRadiusLG}px;
140
- pointer-events: none;
141
- min-width: ${TOOLBAR_ITEM_WIDTH * toolbarCount}px;
142
-
143
- &.nb-in-template {
144
- background: var(--colorTemplateBgSettingsHover);
145
- }
146
-
147
- > .nb-toolbar-container-title {
148
- pointer-events: none;
149
- position: absolute;
150
- font-size: 12px;
151
- padding: 0;
152
- line-height: 16px;
153
- height: 16px;
154
- border-bottom-right-radius: 2px;
155
- border-radius: 2px;
156
- top: 2px;
157
- left: 2px;
158
- display: flex;
159
- align-items: center;
160
- gap: 4px;
161
-
162
- .title-tag {
163
- padding: 0 3px;
164
- border-radius: 2px;
165
- background: var(--colorSettings);
166
- color: #fff;
167
- display: inline-flex;
168
- }
169
- }
170
-
171
- > .nb-toolbar-container-icons {
172
- display: none; // 防止遮挡其它 icons
173
- position: absolute;
174
- right: 2px;
175
- ${toolbarPositionToCSS[toolbarPosition] || ''}
176
- line-height: 16px;
177
- pointer-events: all;
178
-
179
- .ant-space-item {
180
- background-color: var(--colorSettings);
181
- color: #fff;
182
- line-height: 16px;
183
- width: 16px;
184
- height: 16px;
185
- padding: 2px;
186
- display: flex;
187
- align-items: center;
188
- justify-content: center;
189
- }
190
- }
191
-
192
- /* 拖拽把手样式 - 参考 AirTable 样式 */
193
- > .resize-handle {
194
- position: absolute;
195
- pointer-events: all;
196
- background: var(--colorSettings);
197
- opacity: 0.6;
198
- border-radius: 4px;
199
- display: flex;
200
- align-items: center;
201
- justify-content: center;
202
-
203
- &:hover {
204
- opacity: 0.9;
205
- background: var(--colorSettingsHover, var(--colorSettings));
206
- }
207
-
208
- &::before {
209
- content: '';
210
- position: absolute;
211
- background: rgba(255, 255, 255, 0.9);
212
- border-radius: 50%;
213
- }
214
-
215
- &::after {
216
- content: '';
217
- position: absolute;
218
- background: rgba(255, 255, 255, 0.9);
219
- border-radius: 50%;
220
- }
221
- }
222
-
223
- > .resize-handle-left {
224
- left: -4px;
225
- top: 50%;
226
- transform: translateY(-50%);
227
- width: 6px;
228
- height: 20px;
229
- cursor: ew-resize;
230
-
231
- &::before {
232
- width: 2px;
233
- height: 2px;
234
- top: 6px;
235
- left: 50%;
236
- transform: translateX(-50%);
237
- box-shadow:
238
- 0 4px 0 rgba(255, 255, 255, 0.9),
239
- 0 8px 0 rgba(255, 255, 255, 0.9);
240
- }
241
- }
242
-
243
- > .resize-handle-right {
244
- right: -4px;
245
- top: 50%;
246
- transform: translateY(-50%);
247
- width: 6px;
248
- height: 20px;
249
- cursor: ew-resize;
250
-
251
- &::before {
252
- width: 2px;
253
- height: 2px;
254
- top: 6px;
255
- left: 50%;
256
- transform: translateX(-50%);
257
- box-shadow:
258
- 0 4px 0 rgba(255, 255, 255, 0.9),
259
- 0 8px 0 rgba(255, 255, 255, 0.9);
260
- }
261
- }
262
-
263
- > .resize-handle-bottom {
264
- bottom: -4px;
265
- left: 50%;
266
- transform: translateX(-50%);
267
- width: 20px;
268
- height: 6px;
269
- cursor: ns-resize;
270
-
271
- &::before {
272
- width: 2px;
273
- height: 2px;
274
- left: 6px;
275
- top: 50%;
276
- transform: translateY(-50%);
277
- box-shadow:
278
- 4px 0 0 rgba(255, 255, 255, 0.9),
279
- 8px 0 0 rgba(255, 255, 255, 0.9);
280
- }
281
- }
282
- }
283
- `;
331
+ const buildToolbarContainerClassName = ({
332
+ showBackground,
333
+ showBorder,
334
+ ctx,
335
+ portalRenderSnapshot,
336
+ isToolbarVisible,
337
+ className,
338
+ }: {
339
+ showBackground: boolean;
340
+ showBorder: boolean;
341
+ ctx: any;
342
+ portalRenderSnapshot: ToolbarPortalRenderSnapshot | null;
343
+ isToolbarVisible: boolean;
344
+ className?: string;
345
+ }) =>
346
+ [
347
+ toolbarContainerStyles({ showBackground, showBorder, ctx }),
348
+ 'nb-toolbar-portal',
349
+ portalRenderSnapshot?.positioningMode === 'absolute' ? 'nb-toolbar-portal-absolute' : 'nb-toolbar-portal-fixed',
350
+ isToolbarVisible ? 'nb-toolbar-visible' : '',
351
+ className?.includes('nb-in-template') ? 'nb-in-template' : '',
352
+ ]
353
+ .filter(Boolean)
354
+ .join(' ');
355
+
356
+ const buildToolbarContainerStyle = (
357
+ portalRect: ToolbarPortalRect,
358
+ toolbarStyle?: React.CSSProperties,
359
+ ): React.CSSProperties => ({
360
+ top: `${portalRect.top}px`,
361
+ left: `${portalRect.left}px`,
362
+ width: `${portalRect.width}px`,
363
+ height: `${portalRect.height}px`,
364
+ ...omitToolbarPortalInsetStyle(toolbarStyle),
365
+ });
284
366
 
285
- // 悬浮右键菜单组件接口
286
- interface ModelProvidedProps {
367
+ interface ModelProvidedProps extends BaseFloatContextMenuProps {
287
368
  model: FlowModel<any>;
288
- children?: React.ReactNode;
289
- enabled?: boolean;
290
- showDeleteButton?: boolean;
291
- showCopyUidButton?: boolean;
292
- containerStyle?: React.CSSProperties;
293
- toolbarStyle?: React.CSSProperties;
294
- className?: string;
295
- /**
296
- * @default true
297
- */
298
- showBorder?: boolean;
299
- /**
300
- * @default true
301
- */
302
- showBackground?: boolean;
303
- /**
304
- * @default false
305
- */
306
- showTitle?: boolean;
307
- /**
308
- * @default false
309
- */
310
- showDragHandle?: boolean;
311
- /**
312
- * Settings menu levels: 1=current model only (default), 2=include sub-models
313
- */
314
- settingsMenuLevel?: number;
315
- /**
316
- * Extra toolbar items to add to this context menu instance
317
- */
318
- extraToolbarItems?: ToolbarItemConfig[];
319
- /**
320
- * @default 'inside'
321
- */
322
- toolbarPosition?: 'inside' | 'above' | 'below';
323
369
  }
324
370
 
325
- interface ModelByIdProps {
371
+ interface ModelByIdProps extends BaseFloatContextMenuProps {
326
372
  uid: string;
327
373
  modelClassName: string;
328
- children?: React.ReactNode;
329
- enabled?: boolean;
330
- showDeleteButton?: boolean;
331
- showCopyUidButton?: boolean;
332
- containerStyle?: React.CSSProperties;
333
- className?: string;
334
- /**
335
- * @default true
336
- */
337
- showBorder?: boolean;
338
- /**
339
- * @default true
340
- */
341
- showBackground?: boolean;
342
- /**
343
- * @default false
344
- */
345
- showTitle?: boolean;
346
- /**
347
- * Settings menu levels: 1=current model only (default), 2=include sub-models
348
- */
349
- settingsMenuLevel?: number;
350
- /**
351
- * Extra toolbar items to add to this context menu instance
352
- */
353
- extraToolbarItems?: ToolbarItemConfig[];
354
- /**
355
- * @default 'inside'
356
- */
357
- toolbarPosition?: 'inside' | 'above' | 'below';
358
374
  }
359
375
 
360
376
  type FlowsFloatContextMenuProps = ModelProvidedProps | ModelByIdProps;
361
377
 
362
- // 判断是否是通过ID获取模型的props
378
+ // 判断是否是通过ID获取模型的 props
363
379
  const isModelByIdProps = (props: FlowsFloatContextMenuProps): props is ModelByIdProps => {
364
380
  return 'uid' in props && 'modelClassName' in props && Boolean(props.uid) && Boolean(props.modelClassName);
365
381
  };
366
382
 
367
383
  /**
368
- * FlowsFloatContextMenu组件 - 悬浮配置图标组件
384
+ * FlowsFloatContextMenu组件 - 悬浮配置工具栏组件
369
385
  *
370
386
  * 功能特性:
371
387
  * - 鼠标悬浮显示右上角配置图标
372
388
  * - 点击图标显示配置菜单
373
389
  * - 支持删除功能
374
390
  * - Wrapper 模式支持
375
- * - 使用与 NocoBase x-settings 一致的样式
376
- * - 按flow分组显示steps
391
+ * - 使用 portal overlay 避免被宿主或祖先裁剪
392
+ * - 设置菜单与工具栏共享同一个 popup 容器
377
393
  *
378
394
  * 支持两种使用方式:
379
- * 1. 直接提供model: <FlowsFloatContextMenu model={myModel}>{children}</FlowsFloatContextMenu>
380
- * 2. 通过uid和modelClassName获取model: <FlowsFloatContextMenu uid="model1" modelClassName="MyModel">{children}</FlowsFloatContextMenu>
395
+ * 1. 直接提供 model: `<FlowsFloatContextMenu model={myModel}>{children}</FlowsFloatContextMenu>`
396
+ * 2. 通过 uid modelClassName 获取 model:
397
+ * `<FlowsFloatContextMenu uid="model1" modelClassName="MyModel">{children}</FlowsFloatContextMenu>`
381
398
  *
382
399
  * @param props.children 子组件,必须提供
383
- * @param props.enabled 是否启用悬浮菜单,默认为true
384
- * @param props.showDeleteButton 是否显示删除按钮,默认为true
385
- * @param props.showCopyUidButton 是否显示复制UID按钮,默认为true
400
+ * @param props.enabled 是否启用悬浮菜单,默认为 true
401
+ * @param props.showDeleteButton 是否显示删除按钮,默认为 true
402
+ * @param props.showCopyUidButton 是否显示复制 UID 按钮,默认为 true
386
403
  * @param props.containerStyle 容器自定义样式
404
+ * @param props.toolbarStyle 工具栏自定义样式;`top/left/right/bottom` 会作为 portal overlay 的 inset 使用
387
405
  * @param props.className 容器自定义类名
388
- * @param props.showTitle 是否在边框左上角显示模型title,默认为false
406
+ * @param props.showTitle 是否在边框左上角显示模型 title,默认为 false
389
407
  * @param props.settingsMenuLevel 设置菜单层级:1=仅当前模型(默认),2=包含子模型
390
408
  * @param props.extraToolbarItems 额外的工具栏项目,仅应用于此实例
391
409
  */
392
410
  const FlowsFloatContextMenu: React.FC<FlowsFloatContextMenuProps> = observer((props) => {
393
411
  const ctx = useFlowContext();
394
- // Only render if flowSettings is enabled
395
412
  if (!ctx.flowSettingsEnabled) {
396
413
  return <>{props.children}</>;
397
414
  }
398
415
  if (isModelByIdProps(props)) {
399
416
  return <FlowsFloatContextMenuWithModelById {...props} />;
400
- } else {
401
- return <FlowsFloatContextMenuWithModel {...props} />;
402
417
  }
418
+ return <FlowsFloatContextMenuWithModel {...props} />;
403
419
  });
404
420
 
405
- const ResizeHandles: React.FC<{ model: FlowModel; onDragStart: () => void; onDragEnd: () => void }> = (props) => {
421
+ const ResizeHandles: React.FC<{
422
+ model: FlowModel;
423
+ onDragStart: () => void;
424
+ onDragEnd: () => void;
425
+ onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
426
+ onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
427
+ }> = (props) => {
406
428
  const isDraggingRef = useRef<boolean>(false);
407
- const dragTypeRef = useRef<'left' | 'right' | 'bottom' | 'corner' | null>(null);
429
+ const dragTypeRef = useRef<'left' | 'right' | null>(null);
408
430
  const dragStartPosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
409
431
  const { onDragStart, onDragEnd } = props;
410
432
 
411
- // 拖拽移动处理函数
433
+ // 把拖拽位移转成上层已约定的 resize 事件。
412
434
  const handleDragMove = useCallback(
413
435
  (e: MouseEvent) => {
414
436
  if (!isDraggingRef.current || !dragTypeRef.current) return;
415
437
 
416
438
  const deltaX = e.clientX - dragStartPosRef.current.x;
417
- const deltaY = e.clientY - dragStartPosRef.current.y;
418
-
419
- let resizeDistance = 0;
420
439
 
421
440
  switch (dragTypeRef.current) {
422
441
  case 'left':
423
- // 左侧把手:向左拖为正数,向右拖为负数
424
- resizeDistance = -deltaX;
425
- props.model.parent.emitter.emit('onResizeLeft', { resizeDistance, model: props.model });
442
+ props.model.parent.emitter.emit('onResizeLeft', { resizeDistance: -deltaX, model: props.model });
426
443
  break;
427
-
428
444
  case 'right':
429
- // 右侧把手:向右拖为正数,向左拖为负数
430
- resizeDistance = deltaX;
431
- props.model.parent.emitter.emit('onResizeRight', { resizeDistance, model: props.model });
432
- break;
433
-
434
- case 'bottom':
435
- // 底部把手:向下拖为正数,向上拖为负数
436
- resizeDistance = deltaY;
437
- props.model.parent.emitter.emit('onResizeBottom', { resizeDistance, model: props.model });
438
- break;
439
-
440
- case 'corner': {
441
- // 右下角把手:同时计算宽度和高度变化
442
- const widthDelta = deltaX;
443
- const heightDelta = deltaY;
444
- props.model.parent.emitter.emit('onResizeCorner', { widthDelta, heightDelta, model: props.model });
445
+ props.model.parent.emitter.emit('onResizeRight', { resizeDistance: deltaX, model: props.model });
445
446
  break;
446
- }
447
447
  }
448
448
  },
449
449
  [props.model],
450
450
  );
451
451
 
452
- // 拖拽结束处理函数
453
452
  const handleDragEnd = useCallback(() => {
454
453
  isDraggingRef.current = false;
455
454
  dragTypeRef.current = null;
456
455
  dragStartPosRef.current = { x: 0, y: 0 };
457
456
 
458
- // 移除全局事件监听
459
457
  document.removeEventListener('mousemove', handleDragMove);
460
458
  document.removeEventListener('mouseup', handleDragEnd);
461
459
 
462
460
  props.model.parent.emitter.emit('onResizeEnd');
463
461
  onDragEnd?.();
464
- }, [handleDragMove, props.model, onDragEnd]);
462
+ }, [handleDragMove, onDragEnd, props.model]);
465
463
 
466
- // 拖拽开始处理函数
467
464
  const handleDragStart = useCallback(
468
- (e: React.MouseEvent, type: 'left' | 'right' | 'bottom' | 'corner') => {
465
+ (e: React.MouseEvent, type: 'left' | 'right') => {
469
466
  e.preventDefault();
470
467
  e.stopPropagation();
471
468
 
@@ -473,7 +470,6 @@ const ResizeHandles: React.FC<{ model: FlowModel; onDragStart: () => void; onDra
473
470
  dragTypeRef.current = type;
474
471
  dragStartPosRef.current = { x: e.clientX, y: e.clientY };
475
472
 
476
- // 添加全局事件监听
477
473
  document.addEventListener('mousemove', handleDragMove);
478
474
  document.addEventListener('mouseup', handleDragEnd);
479
475
 
@@ -484,27 +480,25 @@ const ResizeHandles: React.FC<{ model: FlowModel; onDragStart: () => void; onDra
484
480
 
485
481
  return (
486
482
  <>
487
- {/* 拖拽把手 */}
488
483
  <div
489
484
  className="resize-handle resize-handle-left"
490
485
  title="拖拽调节宽度"
486
+ onMouseEnter={props.onMouseEnter}
487
+ onMouseLeave={props.onMouseLeave}
491
488
  onMouseDown={(e) => handleDragStart(e, 'left')}
492
- ></div>
489
+ />
493
490
  <div
494
491
  className="resize-handle resize-handle-right"
495
492
  title="拖拽调节宽度"
493
+ onMouseEnter={props.onMouseEnter}
494
+ onMouseLeave={props.onMouseLeave}
496
495
  onMouseDown={(e) => handleDragStart(e, 'right')}
497
- ></div>
498
- {/* <div
499
- className="resize-handle resize-handle-bottom"
500
- title="拖拽调节高度"
501
- onMouseDown={(e) => handleDragStart(e, 'bottom')}
502
- ></div> */}
496
+ />
503
497
  </>
504
498
  );
505
499
  };
506
500
 
507
- // 使用传入的model
501
+ // 使用直接传入的 model 渲染工具栏。
508
502
  const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
509
503
  ({
510
504
  model,
@@ -523,34 +517,94 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
523
517
  toolbarStyle,
524
518
  toolbarPosition = 'inside',
525
519
  }: ModelProvidedProps) => {
526
- const [hideMenu, setHideMenu] = useState<boolean>(false);
527
- const [hasButton, setHasButton] = useState<boolean>(false);
520
+ const [hasButton, setHasButton] = useState(false);
528
521
  const containerRef = useRef<HTMLDivElement>(null);
529
- const flowEngine = useFlowEngine();
530
- const [style, setStyle] = useState<React.CSSProperties>({});
531
522
  const toolbarContainerRef = useRef<HTMLDivElement>(null);
532
- const toolbarContainerStyle: any = useMemo(() => ({ ...toolbarStyle, ...style }), [style, toolbarStyle]);
523
+ const portalActionsRef = useRef<{
524
+ updatePortalRect: () => void;
525
+ schedulePortalRectUpdate: () => void;
526
+ }>({
527
+ updatePortalRect: () => {},
528
+ schedulePortalRectUpdate: () => {},
529
+ });
530
+ const modelUid = getFloatMenuInstanceId(model);
531
+ const flowEngine = useFlowEngine();
532
+ const updatePortalRectProxy = useCallback(() => {
533
+ portalActionsRef.current.updatePortalRect();
534
+ }, []);
535
+ const schedulePortalRectUpdateProxy = useCallback(() => {
536
+ portalActionsRef.current.schedulePortalRectUpdate();
537
+ }, []);
538
+ const {
539
+ isToolbarVisible,
540
+ shouldRenderToolbar,
541
+ handleSettingsMenuOpenChange,
542
+ handleChildHover,
543
+ handleHostMouseEnter,
544
+ handleHostMouseLeave,
545
+ handleToolbarMouseEnter,
546
+ handleToolbarMouseLeave,
547
+ handleResizeDragStart,
548
+ handleResizeDragEnd,
549
+ } = useFloatToolbarVisibility({
550
+ modelUid,
551
+ containerRef,
552
+ toolbarContainerRef,
553
+ updatePortalRect: updatePortalRectProxy,
554
+ schedulePortalRectUpdate: schedulePortalRectUpdateProxy,
555
+ });
556
+ const { portalRect, portalRenderSnapshot, getPopupContainer, updatePortalRect, schedulePortalRectUpdate } =
557
+ useFloatToolbarPortal({
558
+ active: shouldRenderToolbar,
559
+ containerRef,
560
+ toolbarContainerRef,
561
+ toolbarStyle,
562
+ });
563
+
564
+ portalActionsRef.current.updatePortalRect = updatePortalRect;
565
+ portalActionsRef.current.schedulePortalRectUpdate = schedulePortalRectUpdate;
566
+
567
+ const toolbarItems = useMemo(
568
+ () =>
569
+ model
570
+ ? renderToolbarItems(
571
+ model,
572
+ modelUid,
573
+ showDeleteButton,
574
+ showCopyUidButton,
575
+ flowEngine,
576
+ settingsMenuLevel,
577
+ extraToolbarItems,
578
+ handleSettingsMenuOpenChange,
579
+ getPopupContainer,
580
+ )
581
+ : [],
582
+ [
583
+ extraToolbarItems,
584
+ flowEngine,
585
+ getPopupContainer,
586
+ handleSettingsMenuOpenChange,
587
+ model,
588
+ settingsMenuLevel,
589
+ showCopyUidButton,
590
+ showDeleteButton,
591
+ ],
592
+ );
533
593
 
534
- // 检测DOM中是否包含button元素
535
594
  useEffect(() => {
536
- if (containerRef.current) {
537
- const hasButtonElement = detectButtonInDOM(containerRef.current);
538
- setHasButton(hasButtonElement);
595
+ const container = containerRef.current;
596
+ if (!container) {
597
+ return;
539
598
  }
540
- }, [children]); // 当children变化时重新检测
541
599
 
542
- // 使用MutationObserver监听DOM变化
543
- useEffect(() => {
544
- if (!containerRef.current) return;
600
+ const syncHasButton = () => {
601
+ setHasButton(detectButtonInDOM(container));
602
+ };
545
603
 
546
- const observer = new MutationObserver(() => {
547
- if (containerRef.current) {
548
- const hasButtonElement = detectButtonInDOM(containerRef.current);
549
- setHasButton(hasButtonElement);
550
- }
551
- });
604
+ syncHasButton();
552
605
 
553
- observer.observe(containerRef.current, {
606
+ const observer = new MutationObserver(syncHasButton);
607
+ observer.observe(container, {
554
608
  childList: true,
555
609
  subtree: true,
556
610
  attributes: true,
@@ -562,75 +616,79 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
562
616
  };
563
617
  }, []);
564
618
 
565
- const handleChildHover = useCallback((e: React.MouseEvent) => {
566
- const target = e.target as HTMLElement;
567
- const childWithMenu = target.closest('[data-has-float-menu]');
568
-
569
- // 如果悬浮的是子元素(且不是当前容器),则隐藏当前菜单
570
- if (childWithMenu && childWithMenu !== containerRef.current) {
571
- setHideMenu(true);
572
- } else {
573
- setHideMenu(false);
574
- }
575
- }, []);
576
-
577
619
  if (!model) {
578
620
  const t = getT(model || ({} as FlowModel));
579
621
  return <Alert message={t('Invalid model provided')} type="error" />;
580
622
  }
581
623
 
582
- // 如果未启用或没有children,直接返回children
583
624
  if (!enabled || !children) {
584
625
  return <>{children}</>;
585
626
  }
586
627
 
628
+ const toolbarContainerClassName = buildToolbarContainerClassName({
629
+ showBackground,
630
+ showBorder,
631
+ ctx: model.context,
632
+ portalRenderSnapshot,
633
+ isToolbarVisible,
634
+ className,
635
+ });
636
+ const toolbarContainerStyle = buildToolbarContainerStyle(portalRect, toolbarStyle);
637
+
638
+ const toolbarNode = shouldRenderToolbar ? (
639
+ <div
640
+ ref={toolbarContainerRef}
641
+ className={`nb-toolbar-container ${toolbarContainerClassName}`}
642
+ style={toolbarContainerStyle}
643
+ data-model-uid={modelUid}
644
+ >
645
+ {showTitle && (model.title || model.extraTitle) && (
646
+ <div className="nb-toolbar-container-title">
647
+ {model.title && <span className="title-tag">{model.title}</span>}
648
+ {model.extraTitle && <span className="title-tag">{model.extraTitle}</span>}
649
+ </div>
650
+ )}
651
+ <div
652
+ className={`nb-toolbar-container-icons ${toolbarPositionClassNames[toolbarPosition]}`}
653
+ onClick={(e) => e.stopPropagation()}
654
+ onMouseDown={(e) => e.stopPropagation()}
655
+ onMouseMove={(e) => e.stopPropagation()}
656
+ onMouseEnter={handleToolbarMouseEnter}
657
+ onMouseLeave={handleToolbarMouseLeave}
658
+ >
659
+ <Space size={3} align="center">
660
+ {toolbarItems}
661
+ </Space>
662
+ </div>
663
+
664
+ {showDragHandle && (
665
+ <ResizeHandles
666
+ model={model}
667
+ onMouseEnter={handleToolbarMouseEnter}
668
+ onMouseLeave={handleToolbarMouseLeave}
669
+ onDragStart={handleResizeDragStart}
670
+ onDragEnd={handleResizeDragEnd}
671
+ />
672
+ )}
673
+ </div>
674
+ ) : null;
675
+
587
676
  return (
588
677
  <div
589
678
  ref={containerRef}
590
- className={`${floatContainerStyles({
591
- showBackground,
592
- showBorder,
593
- ctx: model.context,
594
- toolbarPosition,
595
- toolbarCount: getToolbarCount(flowEngine, extraToolbarItems),
596
- })} ${hideMenu ? 'hide-parent-menu' : ''} ${hasButton ? 'has-button-child' : ''} ${className || ''}`}
679
+ className={`${hostContainerStyles} ${hasButton ? 'has-button-child' : ''} ${className || ''}`}
597
680
  style={containerStyle}
598
681
  data-has-float-menu="true"
682
+ data-float-menu-model-uid={modelUid}
599
683
  onMouseMove={handleChildHover}
684
+ onMouseEnter={handleHostMouseEnter}
685
+ onMouseLeave={handleHostMouseLeave}
600
686
  >
601
687
  {children}
602
-
603
- {/* 悬浮工具栏 - 使用与 NocoBase 一致的结构 */}
604
- <div ref={toolbarContainerRef} className="nb-toolbar-container" style={toolbarContainerStyle}>
605
- {showTitle && (model.title || model.extraTitle) && (
606
- <div className="nb-toolbar-container-title">
607
- {model.title && <span className="title-tag">{model.title}</span>}
608
- {model.extraTitle && <span className="title-tag">{model.extraTitle}</span>}
609
- </div>
610
- )}
611
- <div
612
- className="nb-toolbar-container-icons"
613
- onClick={(e) => e.stopPropagation()}
614
- onMouseDown={(e) => e.stopPropagation()}
615
- onMouseMove={(e) => e.stopPropagation()}
616
- >
617
- <Space size={3} align="center">
618
- {renderToolbarItems(
619
- model,
620
- showDeleteButton,
621
- showCopyUidButton,
622
- flowEngine,
623
- settingsMenuLevel,
624
- extraToolbarItems,
625
- )}
626
- </Space>
627
- </div>
628
-
629
- {/* 拖拽把手 */}
630
- {showDragHandle && (
631
- <ResizeHandles model={model} onDragStart={() => setStyle({ opacity: 1 })} onDragEnd={() => setStyle({})} />
632
- )}
633
- </div>
688
+ {toolbarNode &&
689
+ (portalRenderSnapshot?.mountElement
690
+ ? createPortal(toolbarNode, portalRenderSnapshot.mountElement)
691
+ : toolbarNode)}
634
692
  </div>
635
693
  );
636
694
  },
@@ -639,22 +697,9 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
639
697
  },
640
698
  );
641
699
 
642
- // 通过useModelById hook获取model
700
+ // 通过 uid + modelClassName 解析 model,再复用主实现。
643
701
  const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
644
- ({
645
- uid,
646
- modelClassName,
647
- children,
648
- enabled = true,
649
- showDeleteButton = true,
650
- showCopyUidButton = true,
651
- containerStyle,
652
- className,
653
- showTitle = false,
654
- settingsMenuLevel,
655
- extraToolbarItems: extraToolbarItems,
656
- toolbarPosition,
657
- }) => {
702
+ ({ uid, modelClassName, children, ...restProps }) => {
658
703
  const model = useFlowModelById(uid, modelClassName);
659
704
  const flowEngine = useFlowEngine();
660
705
 
@@ -663,18 +708,7 @@ const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
663
708
  }
664
709
 
665
710
  return (
666
- <FlowsFloatContextMenuWithModel
667
- model={model}
668
- enabled={enabled}
669
- showDeleteButton={showDeleteButton}
670
- showCopyUidButton={showCopyUidButton}
671
- containerStyle={containerStyle}
672
- className={className}
673
- showTitle={showTitle}
674
- settingsMenuLevel={settingsMenuLevel}
675
- extraToolbarItems={extraToolbarItems}
676
- toolbarPosition={toolbarPosition}
677
- >
711
+ <FlowsFloatContextMenuWithModel model={model} {...restProps}>
678
712
  {children}
679
713
  </FlowsFloatContextMenuWithModel>
680
714
  );
@@ -685,9 +719,3 @@ const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
685
719
  );
686
720
 
687
721
  export { FlowsFloatContextMenu };
688
-
689
- function getToolbarCount(flowEngine, extraToolbarItems) {
690
- const toolbarItems = flowEngine?.flowSettings?.getToolbarItems?.() || [];
691
- const allToolbarItems = [...toolbarItems, ...(extraToolbarItems || [])];
692
- return allToolbarItems.length;
693
- }