@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,8 +8,10 @@
8
8
  */
9
9
 
10
10
  import { DragOutlined } from '@ant-design/icons';
11
+ import type { Modifier } from '@dnd-kit/core';
11
12
  import { DndContext, DndContextProps, DragOverlay, useDraggable, useDroppable } from '@dnd-kit/core';
12
- import React, { FC, useState } from 'react';
13
+ import type { Transform } from '@dnd-kit/utilities';
14
+ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
13
15
  import { createPortal } from 'react-dom';
14
16
  import { FlowModel } from '../../models';
15
17
  import { useFlowEngine } from '../../provider';
@@ -19,18 +21,229 @@ export * from './findModelUidPosition';
19
21
  export * from './gridDragPlanner';
20
22
 
21
23
  export const EMPTY_COLUMN_UID = 'EMPTY_COLUMN';
24
+ export const TOOLBAR_DRAG_ACTIVITY_EVENT = 'nb-toolbar-drag-activity';
25
+ const TOOLBAR_DRAG_ANCHOR_EVENT = 'nb-toolbar-drag-anchor';
26
+ const MENU_SUBMENU_POPUP_SELECTOR = '.ant-menu-submenu-popup';
27
+
28
+ type ToolbarDragAnchorPoint = {
29
+ x: number;
30
+ y: number;
31
+ };
32
+
33
+ type ToolbarDragAnchorDetail = {
34
+ modelUid: string;
35
+ point: ToolbarDragAnchorPoint | null;
36
+ };
37
+
38
+ export const resolveOverlayAnchorTransform = ({
39
+ activeId,
40
+ active,
41
+ transform,
42
+ activeNodeRect,
43
+ dragAnchorPoint,
44
+ }: {
45
+ activeId: string | null;
46
+ active: { id: string | number } | null | undefined;
47
+ transform: Transform;
48
+ activeNodeRect: { top: number; left: number } | null;
49
+ dragAnchorPoint: ToolbarDragAnchorPoint | null;
50
+ }): Transform => {
51
+ if (!activeId || active?.id !== activeId || !dragAnchorPoint || !activeNodeRect) {
52
+ return transform;
53
+ }
54
+
55
+ return {
56
+ ...transform,
57
+ x: transform.x + dragAnchorPoint.x - activeNodeRect.left,
58
+ y: transform.y + dragAnchorPoint.y - activeNodeRect.top,
59
+ };
60
+ };
61
+
62
+ const resolveDraggableHostNode = (activatorNode: HTMLElement | null) => {
63
+ const ownerDocument = activatorNode?.ownerDocument;
64
+ const floatToolbarContainer = activatorNode?.closest<HTMLElement>('.nb-toolbar-container[data-model-uid]');
65
+ const toolbarModelUid = floatToolbarContainer?.getAttribute('data-model-uid');
66
+
67
+ if (!ownerDocument || !toolbarModelUid) {
68
+ return activatorNode;
69
+ }
70
+
71
+ const matchedHosts = Array.from(
72
+ ownerDocument.querySelectorAll<HTMLElement>(
73
+ `[data-has-float-menu="true"][data-float-menu-model-uid="${toolbarModelUid}"]`,
74
+ ),
75
+ );
76
+ const popupRoot = floatToolbarContainer?.closest<HTMLElement>(MENU_SUBMENU_POPUP_SELECTOR);
77
+
78
+ if (popupRoot) {
79
+ return (
80
+ matchedHosts.find((hostNode) => hostNode.closest(MENU_SUBMENU_POPUP_SELECTOR) === popupRoot) || activatorNode
81
+ );
82
+ }
83
+
84
+ return (
85
+ matchedHosts.find((hostNode) => !hostNode.closest(MENU_SUBMENU_POPUP_SELECTOR)) || matchedHosts[0] || activatorNode
86
+ );
87
+ };
22
88
 
23
89
  // 可拖拽图标组件
24
- export const DragHandler: FC<{ model: FlowModel; children: React.ReactNode }> = ({
90
+ export const DragHandler: FC<{ model: FlowModel; children?: React.ReactNode }> = ({
25
91
  model,
26
92
  children = <DragOutlined />,
27
93
  }) => {
28
- const { attributes, listeners, setNodeRef } = useDraggable({ id: model.uid });
94
+ const { attributes, isDragging, listeners, setActivatorNodeRef, setNodeRef } = useDraggable({ id: model.uid });
95
+ const dragHandlerRef = useRef<HTMLSpanElement | null>(null);
96
+ const draggableNodeRef = useRef<HTMLElement | null>(null);
97
+ const pointerPressCleanupRef = useRef<(() => void) | null>(null);
98
+ const isDraggingRef = useRef(isDragging);
99
+ const isPointerPressActiveRef = useRef(false);
100
+ const isToolbarDragActiveRef = useRef(false);
101
+ const syncDraggableNodeRef = useCallback(
102
+ (activatorNode: HTMLElement | null) => {
103
+ const nextNode = resolveDraggableHostNode(activatorNode);
104
+
105
+ if (draggableNodeRef.current === nextNode) {
106
+ return;
107
+ }
108
+
109
+ draggableNodeRef.current = nextNode;
110
+ setNodeRef(nextNode);
111
+ },
112
+ [setNodeRef],
113
+ );
114
+ const setDragHandlerNodeRef = useCallback(
115
+ (node: HTMLSpanElement | null) => {
116
+ dragHandlerRef.current = node;
117
+ setActivatorNodeRef(node);
118
+ syncDraggableNodeRef(node);
119
+ },
120
+ [setActivatorNodeRef, syncDraggableNodeRef],
121
+ );
122
+ const dispatchToolbarDragActivity = useCallback(
123
+ (active: boolean) => {
124
+ const ownerDocument = dragHandlerRef.current?.ownerDocument;
125
+ if (!ownerDocument) {
126
+ return;
127
+ }
128
+
129
+ ownerDocument.dispatchEvent(
130
+ new CustomEvent(TOOLBAR_DRAG_ACTIVITY_EVENT, {
131
+ detail: { active, modelUid: model.uid },
132
+ }),
133
+ );
134
+ },
135
+ [model.uid],
136
+ );
137
+
138
+ const dispatchToolbarDragAnchor = useCallback(
139
+ (detail: Pick<ToolbarDragAnchorDetail, 'point'>) => {
140
+ const ownerDocument = dragHandlerRef.current?.ownerDocument;
141
+ if (!ownerDocument) {
142
+ return;
143
+ }
144
+
145
+ ownerDocument.dispatchEvent(
146
+ new CustomEvent<ToolbarDragAnchorDetail>(TOOLBAR_DRAG_ANCHOR_EVENT, {
147
+ detail: { modelUid: model.uid, point: detail.point },
148
+ }),
149
+ );
150
+ },
151
+ [model.uid],
152
+ );
153
+
154
+ const clearPointerPressListeners = useCallback(() => {
155
+ pointerPressCleanupRef.current?.();
156
+ pointerPressCleanupRef.current = null;
157
+ }, []);
158
+
159
+ const syncToolbarDragActivity = useCallback(() => {
160
+ const nextActive = isPointerPressActiveRef.current || isDraggingRef.current;
161
+ if (nextActive === isToolbarDragActiveRef.current) {
162
+ return;
163
+ }
164
+
165
+ isToolbarDragActiveRef.current = nextActive;
166
+ dispatchToolbarDragActivity(nextActive);
167
+ }, [dispatchToolbarDragActivity]);
168
+
169
+ const handlePointerPressEnd = useCallback(() => {
170
+ isPointerPressActiveRef.current = false;
171
+ clearPointerPressListeners();
172
+ syncToolbarDragActivity();
173
+ }, [clearPointerPressListeners, syncToolbarDragActivity]);
174
+
175
+ const registerPointerPressListeners = useCallback(() => {
176
+ const ownerDocument = dragHandlerRef.current?.ownerDocument;
177
+ const ownerWindow = ownerDocument?.defaultView;
178
+ if (!ownerDocument) {
179
+ return;
180
+ }
181
+
182
+ clearPointerPressListeners();
183
+
184
+ const handlePointerEnd = () => {
185
+ handlePointerPressEnd();
186
+ };
187
+ const handleKeyDown = (event: KeyboardEvent) => {
188
+ if (event.key === 'Escape') {
189
+ handlePointerPressEnd();
190
+ }
191
+ };
192
+
193
+ ownerDocument.addEventListener('pointerup', handlePointerEnd, true);
194
+ ownerDocument.addEventListener('pointercancel', handlePointerEnd, true);
195
+ ownerDocument.addEventListener('keydown', handleKeyDown, true);
196
+ ownerWindow?.addEventListener('blur', handlePointerEnd);
197
+
198
+ pointerPressCleanupRef.current = () => {
199
+ ownerDocument.removeEventListener('pointerup', handlePointerEnd, true);
200
+ ownerDocument.removeEventListener('pointercancel', handlePointerEnd, true);
201
+ ownerDocument.removeEventListener('keydown', handleKeyDown, true);
202
+ ownerWindow?.removeEventListener('blur', handlePointerEnd);
203
+ };
204
+ }, [clearPointerPressListeners, handlePointerPressEnd]);
205
+
206
+ useEffect(() => {
207
+ syncDraggableNodeRef(dragHandlerRef.current);
208
+ }, [syncDraggableNodeRef]);
209
+
210
+ useEffect(() => {
211
+ isDraggingRef.current = isDragging;
212
+ syncToolbarDragActivity();
213
+ }, [isDragging, syncToolbarDragActivity]);
214
+
215
+ useEffect(() => {
216
+ return () => {
217
+ if (isToolbarDragActiveRef.current) {
218
+ dispatchToolbarDragActivity(false);
219
+ }
220
+ isPointerPressActiveRef.current = false;
221
+ isDraggingRef.current = false;
222
+ isToolbarDragActiveRef.current = false;
223
+ clearPointerPressListeners();
224
+ };
225
+ }, [clearPointerPressListeners, dispatchToolbarDragActivity]);
226
+
29
227
  return (
30
228
  <span
31
- ref={setNodeRef}
229
+ ref={setDragHandlerNodeRef}
32
230
  {...listeners}
33
231
  {...attributes}
232
+ onPointerDownCapture={(event) => {
233
+ if (event.button !== 0) {
234
+ return;
235
+ }
236
+
237
+ dispatchToolbarDragAnchor({
238
+ point: {
239
+ x: event.clientX,
240
+ y: event.clientY,
241
+ },
242
+ });
243
+ isPointerPressActiveRef.current = true;
244
+ syncToolbarDragActivity();
245
+ registerPointerPressListeners();
246
+ }}
34
247
  style={{
35
248
  cursor: 'grab',
36
249
  }}
@@ -74,19 +287,56 @@ export const Droppable: FC<{ model: FlowModel<any>; children: React.ReactNode }>
74
287
  export const DndProvider: FC<DndContextProps & PersistOptions> = ({
75
288
  persist = true,
76
289
  children,
290
+ onDragStart,
77
291
  onDragEnd,
292
+ onDragCancel,
78
293
  ...restProps
79
294
  }) => {
80
295
  const [activeId, setActiveId] = useState<string | null>(null);
296
+ const [dragAnchorPoint, setDragAnchorPoint] = useState<ToolbarDragAnchorDetail['point']>(null);
81
297
  const flowEngine = useFlowEngine();
298
+
299
+ useEffect(() => {
300
+ if (typeof document === 'undefined') {
301
+ return;
302
+ }
303
+
304
+ const handleToolbarDragAnchor = (event: Event) => {
305
+ const customEvent = event as CustomEvent<ToolbarDragAnchorDetail>;
306
+ setDragAnchorPoint(customEvent.detail?.point || null);
307
+ };
308
+
309
+ document.addEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
310
+ return () => {
311
+ document.removeEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
312
+ };
313
+ }, []);
314
+
315
+ const overlayAnchorModifier = useCallback<Modifier>(
316
+ ({ active, activeNodeRect, transform }) => {
317
+ const nextTransform: Transform = resolveOverlayAnchorTransform({
318
+ activeId,
319
+ active,
320
+ transform,
321
+ activeNodeRect,
322
+ dragAnchorPoint,
323
+ });
324
+
325
+ return nextTransform;
326
+ },
327
+ [activeId, dragAnchorPoint],
328
+ );
329
+
82
330
  return (
83
331
  <DndContext
332
+ {...restProps}
84
333
  onDragStart={(event) => {
85
334
  setActiveId(event.active.id as string);
86
- restProps.onDragStart?.(event);
335
+ onDragStart?.(event);
87
336
  }}
88
337
  onDragEnd={(event) => {
89
338
  setActiveId(null);
339
+ setDragAnchorPoint(null);
90
340
  // 如果没有 onDragEnd 回调,则默认调用 flowEngine 的 moveModel 方法
91
341
  if (!onDragEnd) {
92
342
  if (event.over) {
@@ -97,32 +347,46 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
97
347
  onDragEnd(event);
98
348
  }
99
349
  }}
350
+ onDragCancel={(event) => {
351
+ setActiveId(null);
352
+ setDragAnchorPoint(null);
353
+ onDragCancel?.(event);
354
+ }}
100
355
  {...restProps}
101
356
  >
102
357
  {children}
103
- {createPortal(
104
- <DragOverlay dropAnimation={null} zIndex={2000}>
105
- {activeId && (
106
- <span
107
- style={{
108
- display: 'inline-flex',
109
- alignItems: 'center',
110
- whiteSpace: 'nowrap',
111
- background: '#fff',
112
- border: '1px solid #1890ff',
113
- borderRadius: 4,
114
- padding: '4px 12px',
115
- color: '#1890ff',
116
- // fontSize: 18,
117
- boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
118
- }}
358
+ {typeof document !== 'undefined'
359
+ ? createPortal(
360
+ <DragOverlay
361
+ dropAnimation={null}
362
+ modifiers={[overlayAnchorModifier]}
363
+ zIndex={2000}
364
+ style={{ pointerEvents: 'none' }}
119
365
  >
120
- {flowEngine.translate('Dragging')}
121
- </span>
122
- )}
123
- </DragOverlay>,
124
- document.body,
125
- )}
366
+ {activeId && (
367
+ <span
368
+ data-testid="flow-drag-preview"
369
+ style={{
370
+ display: 'inline-flex',
371
+ alignItems: 'center',
372
+ whiteSpace: 'nowrap',
373
+ background: '#fff',
374
+ border: '1px solid #1890ff',
375
+ borderRadius: 4,
376
+ padding: '4px 12px',
377
+ color: '#1890ff',
378
+ pointerEvents: 'none',
379
+ // fontSize: 18,
380
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
381
+ }}
382
+ >
383
+ {flowEngine.translate('Dragging')}
384
+ </span>
385
+ )}
386
+ </DragOverlay>,
387
+ document.body,
388
+ )
389
+ : null}
126
390
  </DndContext>
127
391
  );
128
392
  };
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { Select } from 'antd';
10
+ import { Select, Tooltip } from 'antd';
11
11
  import React, { useEffect, useRef, useState } from 'react';
12
12
  import { useFlowEngineContext } from '../../../../provider';
13
13
 
@@ -19,6 +19,7 @@ export interface SelectWithTitleProps {
19
19
  itemKey?: string;
20
20
  onChange?: (...args: any[]) => void;
21
21
  dropdownRender?: any;
22
+ tooltip?: any;
22
23
  }
23
24
 
24
25
  export function SelectWithTitle({
@@ -28,6 +29,7 @@ export function SelectWithTitle({
28
29
  options,
29
30
  fieldNames,
30
31
  itemKey,
32
+ tooltip,
31
33
  ...others
32
34
  }: SelectWithTitleProps) {
33
35
  const [open, setOpen] = useState(false);
@@ -66,6 +68,17 @@ export function SelectWithTitle({
66
68
  setValue(val);
67
69
  onChange?.({ [itemKey]: val });
68
70
  };
71
+ const titleNode = (
72
+ <span
73
+ style={{
74
+ whiteSpace: 'nowrap', // 不换行
75
+ flexShrink: 0, // 不被挤压
76
+ }}
77
+ >
78
+ {title}
79
+ </span>
80
+ );
81
+
69
82
  return (
70
83
  <div
71
84
  style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}
@@ -79,14 +92,13 @@ export function SelectWithTitle({
79
92
  }, 200);
80
93
  }}
81
94
  >
82
- <span
83
- style={{
84
- whiteSpace: 'nowrap', // 不换行
85
- flexShrink: 0, // 不被挤压
86
- }}
87
- >
88
- {title}
89
- </span>
95
+ {tooltip ? (
96
+ <Tooltip title={tooltip} placement="top" destroyTooltipOnHide>
97
+ {titleNode}
98
+ </Tooltip>
99
+ ) : (
100
+ titleNode
101
+ )}
90
102
  <Select
91
103
  {...others}
92
104
  open={open}
@@ -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,42 @@ 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
+
194
240
  export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
195
241
  model,
196
242
  showDeleteButton = true,
197
243
  showCopyUidButton = true,
198
244
  menuLevels = 1, // 默认一级菜单
199
245
  flattenSubMenus = true,
246
+ onDropdownVisibleChange,
247
+ getPopupContainer,
200
248
  }) => {
201
249
  const { message } = App.useApp();
202
250
  const t = useMemo(() => getT(model), [model]);
@@ -210,15 +258,38 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
210
258
  const [isLoading, setIsLoading] = useState(true);
211
259
  const closeDropdown = useCallback(() => {
212
260
  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
- }, []);
261
+ onDropdownVisibleChange?.(false);
262
+ }, [onDropdownVisibleChange]);
263
+ const resolvePopupContainer = useCallback<NonNullable<DropdownProps['getPopupContainer']>>(
264
+ (triggerNode) => {
265
+ // 工具栏自身容器必须优先,保证鼠标从 icon 移到菜单时仍处于同一 hover 树。
266
+ // 弹窗场景的裁剪问题由 useFloatToolbarPortal 负责把 toolbar 挂到正确的 popup host。
267
+ return (
268
+ getToolbarPopupContainer(triggerNode) ||
269
+ getPopupContainer?.(triggerNode) ||
270
+ triggerNode?.parentElement ||
271
+ document.body
272
+ );
273
+ },
274
+ [getPopupContainer],
275
+ );
276
+ const handleOpenChange: DropdownProps['onOpenChange'] = useCallback(
277
+ (nextOpen: boolean, info) => {
278
+ if (info.source === 'trigger' || nextOpen) {
279
+ // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
280
+ startTransition(() => {
281
+ setVisible(nextOpen);
282
+ });
283
+ onDropdownVisibleChange?.(nextOpen);
284
+ }
285
+ },
286
+ [onDropdownVisibleChange],
287
+ );
288
+ useEffect(() => {
289
+ return () => {
290
+ onDropdownVisibleChange?.(false);
291
+ };
292
+ }, [onDropdownVisibleChange]);
222
293
  const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]);
223
294
  useEffect(() => {
224
295
  let mounted = true;
@@ -422,7 +493,11 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
422
493
  return;
423
494
  }
424
495
 
425
- const extra = extraMenuItems.find((it) => it?.key === originalKey || it?.key === cleanKey);
496
+ const extra =
497
+ findExtraMenuItemByKey(extraMenuItems, originalKey) || findExtraMenuItemByKey(extraMenuItems, cleanKey);
498
+ if (extra?.disabled) {
499
+ return;
500
+ }
426
501
  if (extra?.onClick) {
427
502
  closeDropdown();
428
503
  extra.onClick();
@@ -833,6 +908,9 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
833
908
 
834
909
  return (
835
910
  <Dropdown
911
+ getPopupContainer={resolvePopupContainer}
912
+ overlayClassName={TOOLBAR_DROPDOWN_OVERLAY_CLASS}
913
+ overlayStyle={{ width: 'max-content', minWidth: 'max-content' }}
836
914
  onOpenChange={handleOpenChange}
837
915
  open={visible}
838
916
  menu={{