@nocobase/flow-engine 2.1.0-alpha.3 → 2.1.0-alpha.30

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