@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45

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 (209) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/FlowContextProvider.d.ts +5 -1
  4. package/lib/FlowContextProvider.js +9 -2
  5. package/lib/JSRunner.d.ts +10 -1
  6. package/lib/JSRunner.js +50 -5
  7. package/lib/ViewScopedFlowEngine.js +5 -1
  8. package/lib/components/FieldModelRenderer.js +2 -2
  9. package/lib/components/FlowModelRenderer.d.ts +3 -1
  10. package/lib/components/FlowModelRenderer.js +12 -6
  11. package/lib/components/FormItem.d.ts +6 -0
  12. package/lib/components/FormItem.js +11 -3
  13. package/lib/components/MobilePopup.js +6 -5
  14. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  15. package/lib/components/dnd/gridDragPlanner.js +613 -21
  16. package/lib/components/dnd/index.d.ts +31 -2
  17. package/lib/components/dnd/index.js +244 -23
  18. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  19. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  20. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  21. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -11
  22. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  23. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  24. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  25. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  26. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  27. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  28. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  29. package/lib/components/subModel/AddSubModelButton.js +27 -1
  30. package/lib/components/subModel/LazyDropdown.js +293 -52
  31. package/lib/components/subModel/index.d.ts +1 -0
  32. package/lib/components/subModel/index.js +19 -0
  33. package/lib/components/subModel/utils.d.ts +1 -1
  34. package/lib/components/subModel/utils.js +9 -3
  35. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  36. package/lib/components/variables/VariableHybridInput.js +499 -0
  37. package/lib/components/variables/index.d.ts +2 -0
  38. package/lib/components/variables/index.js +3 -0
  39. package/lib/data-source/index.d.ts +84 -0
  40. package/lib/data-source/index.js +259 -5
  41. package/lib/executor/FlowExecutor.js +32 -9
  42. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  43. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  44. package/lib/flow-registry/index.d.ts +1 -0
  45. package/lib/flow-registry/index.js +3 -1
  46. package/lib/flowContext.d.ts +3 -0
  47. package/lib/flowContext.js +46 -1
  48. package/lib/flowEngine.d.ts +151 -1
  49. package/lib/flowEngine.js +392 -18
  50. package/lib/flowI18n.js +2 -1
  51. package/lib/flowSettings.d.ts +14 -6
  52. package/lib/flowSettings.js +34 -6
  53. package/lib/index.d.ts +2 -0
  54. package/lib/index.js +7 -0
  55. package/lib/lazy-helper.d.ts +14 -0
  56. package/lib/lazy-helper.js +71 -0
  57. package/lib/locale/en-US.json +1 -0
  58. package/lib/locale/index.d.ts +2 -0
  59. package/lib/locale/zh-CN.json +1 -0
  60. package/lib/models/DisplayItemModel.d.ts +1 -1
  61. package/lib/models/EditableItemModel.d.ts +1 -1
  62. package/lib/models/FilterableItemModel.d.ts +1 -1
  63. package/lib/models/flowModel.d.ts +13 -10
  64. package/lib/models/flowModel.js +81 -21
  65. package/lib/provider.js +38 -23
  66. package/lib/reactive/observer.js +46 -16
  67. package/lib/runjs-context/registry.d.ts +1 -1
  68. package/lib/runjs-context/setup.js +20 -12
  69. package/lib/runjs-context/snippets/index.js +13 -2
  70. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  71. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  72. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  74. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  75. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  82. package/lib/utils/parsePathnameToViewParams.js +29 -5
  83. package/lib/utils/randomId.d.ts +39 -0
  84. package/lib/utils/randomId.js +45 -0
  85. package/lib/utils/runjsTemplateCompat.js +1 -1
  86. package/lib/utils/runjsValue.js +41 -11
  87. package/lib/utils/schema-utils.d.ts +7 -1
  88. package/lib/utils/schema-utils.js +19 -0
  89. package/lib/views/FlowView.d.ts +7 -1
  90. package/lib/views/FlowView.js +11 -1
  91. package/lib/views/PageComponent.js +8 -6
  92. package/lib/views/ViewNavigation.d.ts +12 -2
  93. package/lib/views/ViewNavigation.js +28 -9
  94. package/lib/views/createViewMeta.js +114 -50
  95. package/lib/views/inheritLayoutContext.d.ts +10 -0
  96. package/lib/views/inheritLayoutContext.js +50 -0
  97. package/lib/views/runViewBeforeClose.d.ts +10 -0
  98. package/lib/views/runViewBeforeClose.js +45 -0
  99. package/lib/views/useDialog.d.ts +2 -1
  100. package/lib/views/useDialog.js +22 -3
  101. package/lib/views/useDrawer.d.ts +2 -1
  102. package/lib/views/useDrawer.js +22 -3
  103. package/lib/views/usePage.d.ts +5 -11
  104. package/lib/views/usePage.js +304 -144
  105. package/package.json +6 -5
  106. package/src/FlowContextProvider.tsx +9 -1
  107. package/src/JSRunner.ts +68 -4
  108. package/src/ViewScopedFlowEngine.ts +4 -0
  109. package/src/__tests__/JSRunner.test.ts +27 -1
  110. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  111. package/src/__tests__/flow-engine.test.ts +166 -0
  112. package/src/__tests__/flowContext.test.ts +82 -1
  113. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +16 -0
  120. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  121. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  122. package/src/__tests__/runjsSnippets.test.ts +21 -0
  123. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  124. package/src/components/FieldModelRenderer.tsx +2 -1
  125. package/src/components/FlowModelRenderer.tsx +18 -6
  126. package/src/components/FormItem.tsx +7 -1
  127. package/src/components/MobilePopup.tsx +4 -2
  128. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  129. package/src/components/__tests__/FormItem.test.tsx +25 -0
  130. package/src/components/__tests__/dnd.test.ts +44 -0
  131. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  132. package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
  133. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  134. package/src/components/dnd/gridDragPlanner.ts +758 -19
  135. package/src/components/dnd/index.tsx +305 -28
  136. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  137. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +99 -11
  138. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  139. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +194 -5
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +32 -2
  145. package/src/components/subModel/LazyDropdown.tsx +332 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +7 -1
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +68 -1
  154. package/src/data-source/index.ts +322 -6
  155. package/src/executor/FlowExecutor.ts +35 -10
  156. package/src/executor/__tests__/flowExecutor.test.ts +85 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +50 -3
  161. package/src/flowEngine.ts +449 -14
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
  172. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  173. package/src/models/__tests__/flowModel.test.ts +80 -37
  174. package/src/models/flowModel.tsx +122 -36
  175. package/src/provider.tsx +41 -25
  176. package/src/reactive/__tests__/observer.test.tsx +82 -0
  177. package/src/reactive/observer.tsx +87 -25
  178. package/src/runjs-context/registry.ts +1 -1
  179. package/src/runjs-context/setup.ts +22 -12
  180. package/src/runjs-context/snippets/index.ts +12 -1
  181. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  182. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  183. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  184. package/src/types.ts +62 -0
  185. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  186. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +28 -0
  187. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  188. package/src/utils/__tests__/utils.test.ts +62 -0
  189. package/src/utils/createCollectionContextMeta.ts +6 -2
  190. package/src/utils/index.ts +5 -1
  191. package/src/utils/parsePathnameToViewParams.ts +47 -7
  192. package/src/utils/randomId.ts +48 -0
  193. package/src/utils/runjsTemplateCompat.ts +1 -1
  194. package/src/utils/runjsValue.ts +50 -11
  195. package/src/utils/schema-utils.ts +30 -1
  196. package/src/views/FlowView.tsx +22 -2
  197. package/src/views/PageComponent.tsx +7 -4
  198. package/src/views/ViewNavigation.ts +46 -9
  199. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  200. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  201. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  202. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  203. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  204. package/src/views/createViewMeta.ts +106 -34
  205. package/src/views/inheritLayoutContext.ts +26 -0
  206. package/src/views/runViewBeforeClose.ts +19 -0
  207. package/src/views/useDialog.tsx +27 -3
  208. package/src/views/useDrawer.tsx +27 -3
  209. package/src/views/usePage.tsx +367 -179
@@ -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
  }}
@@ -70,23 +283,73 @@ export const Droppable: FC<{ model: FlowModel<any>; children: React.ReactNode }>
70
283
  );
71
284
  };
72
285
 
286
+ export interface DndProviderProps extends DndContextProps, PersistOptions {
287
+ /**
288
+ * Whether to render the built-in `DragOverlay` (the "Dragging" pill that
289
+ * follows the cursor). Defaults to `true` for backwards compatibility with
290
+ * existing flow-engine drag interactions. Set to `false` when the
291
+ * surrounding UI already gives clear visual feedback (e.g. a drag-sort
292
+ * table that highlights the drop position) and the floating pill would be
293
+ * redundant.
294
+ */
295
+ showDragOverlay?: boolean;
296
+ }
297
+
73
298
  // 提供一个封装了 DragOverlay 的 DndProvider 组件,继承 DndContext 的所有 props
74
- export const DndProvider: FC<DndContextProps & PersistOptions> = ({
299
+ export const DndProvider: FC<DndProviderProps> = ({
75
300
  persist = true,
301
+ showDragOverlay = true,
76
302
  children,
303
+ onDragStart,
77
304
  onDragEnd,
305
+ onDragCancel,
78
306
  ...restProps
79
307
  }) => {
80
308
  const [activeId, setActiveId] = useState<string | null>(null);
309
+ const [dragAnchorPoint, setDragAnchorPoint] = useState<ToolbarDragAnchorDetail['point']>(null);
81
310
  const flowEngine = useFlowEngine();
311
+
312
+ useEffect(() => {
313
+ if (typeof document === 'undefined') {
314
+ return;
315
+ }
316
+
317
+ const handleToolbarDragAnchor = (event: Event) => {
318
+ const customEvent = event as CustomEvent<ToolbarDragAnchorDetail>;
319
+ setDragAnchorPoint(customEvent.detail?.point || null);
320
+ };
321
+
322
+ document.addEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
323
+ return () => {
324
+ document.removeEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
325
+ };
326
+ }, []);
327
+
328
+ const overlayAnchorModifier = useCallback<Modifier>(
329
+ ({ active, activeNodeRect, transform }) => {
330
+ const nextTransform: Transform = resolveOverlayAnchorTransform({
331
+ activeId,
332
+ active,
333
+ transform,
334
+ activeNodeRect,
335
+ dragAnchorPoint,
336
+ });
337
+
338
+ return nextTransform;
339
+ },
340
+ [activeId, dragAnchorPoint],
341
+ );
342
+
82
343
  return (
83
344
  <DndContext
345
+ {...restProps}
84
346
  onDragStart={(event) => {
85
347
  setActiveId(event.active.id as string);
86
- restProps.onDragStart?.(event);
348
+ onDragStart?.(event);
87
349
  }}
88
350
  onDragEnd={(event) => {
89
351
  setActiveId(null);
352
+ setDragAnchorPoint(null);
90
353
  // 如果没有 onDragEnd 回调,则默认调用 flowEngine 的 moveModel 方法
91
354
  if (!onDragEnd) {
92
355
  if (event.over) {
@@ -97,32 +360,46 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
97
360
  onDragEnd(event);
98
361
  }
99
362
  }}
363
+ onDragCancel={(event) => {
364
+ setActiveId(null);
365
+ setDragAnchorPoint(null);
366
+ onDragCancel?.(event);
367
+ }}
100
368
  {...restProps}
101
369
  >
102
370
  {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
- }}
371
+ {showDragOverlay && typeof document !== 'undefined'
372
+ ? createPortal(
373
+ <DragOverlay
374
+ dropAnimation={null}
375
+ modifiers={[overlayAnchorModifier]}
376
+ zIndex={2000}
377
+ style={{ pointerEvents: 'none' }}
119
378
  >
120
- {flowEngine.translate('Dragging')}
121
- </span>
122
- )}
123
- </DragOverlay>,
124
- document.body,
125
- )}
379
+ {activeId && (
380
+ <span
381
+ data-testid="flow-drag-preview"
382
+ style={{
383
+ display: 'inline-flex',
384
+ alignItems: 'center',
385
+ whiteSpace: 'nowrap',
386
+ background: '#fff',
387
+ border: '1px solid #1890ff',
388
+ borderRadius: 4,
389
+ padding: '4px 12px',
390
+ color: '#1890ff',
391
+ pointerEvents: 'none',
392
+ // fontSize: 18,
393
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
394
+ }}
395
+ >
396
+ {flowEngine.translate('Dragging')}
397
+ </span>
398
+ )}
399
+ </DragOverlay>,
400
+ document.body,
401
+ )
402
+ : null}
126
403
  </DndContext>
127
404
  );
128
405
  };
@@ -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,51 @@ interface DefaultSettingsIconProps {
188
209
  showCopyUidButton?: boolean;
189
210
  menuLevels?: number; // Menu levels: 1=current model only (default), 2=include sub-models
190
211
  flattenSubMenus?: boolean; // Whether to flatten sub-menus: false=group by model (default), true=flatten all
212
+ onDropdownVisibleChange?: (open: boolean) => void;
213
+ getPopupContainer?: DropdownProps['getPopupContainer'];
191
214
  [key: string]: any; // Allow additional props
192
215
  }
193
216
 
217
+ const TOOLBAR_ICONS_SELECTOR = '.nb-toolbar-container-icons';
218
+ const TOOLBAR_CONTAINER_SELECTOR = '.nb-toolbar-container';
219
+ const TOOLBAR_DROPDOWN_OVERLAY_CLASS = css`
220
+ width: max-content;
221
+ min-width: max-content;
222
+
223
+ .ant-dropdown-menu {
224
+ width: max-content;
225
+ min-width: max-content;
226
+ }
227
+ `;
228
+
229
+ const getToolbarPopupContainer = (triggerNode?: HTMLElement | null) => {
230
+ if (!triggerNode) {
231
+ return null;
232
+ }
233
+
234
+ return (
235
+ (triggerNode.closest(TOOLBAR_ICONS_SELECTOR) as HTMLElement | null) ||
236
+ (triggerNode.closest(TOOLBAR_CONTAINER_SELECTOR) as HTMLElement | null)
237
+ );
238
+ };
239
+
240
+ const removeExtraMenuItemClickHandlers = (item: FlowModelExtraMenuItem): FlowModelExtraMenuItem => {
241
+ const { onClick: _onClick, children, ...rest } = item;
242
+
243
+ return {
244
+ ...rest,
245
+ children: children?.length ? children.map(removeExtraMenuItemClickHandlers) : undefined,
246
+ };
247
+ };
248
+
194
249
  export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
195
250
  model,
196
251
  showDeleteButton = true,
197
252
  showCopyUidButton = true,
198
253
  menuLevels = 1, // 默认一级菜单
199
254
  flattenSubMenus = true,
255
+ onDropdownVisibleChange,
256
+ getPopupContainer,
200
257
  }) => {
201
258
  const { message } = App.useApp();
202
259
  const t = useMemo(() => getT(model), [model]);
@@ -210,15 +267,38 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
210
267
  const [isLoading, setIsLoading] = useState(true);
211
268
  const closeDropdown = useCallback(() => {
212
269
  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
- }, []);
270
+ onDropdownVisibleChange?.(false);
271
+ }, [onDropdownVisibleChange]);
272
+ const resolvePopupContainer = useCallback<NonNullable<DropdownProps['getPopupContainer']>>(
273
+ (triggerNode) => {
274
+ // 工具栏自身容器必须优先,保证鼠标从 icon 移到菜单时仍处于同一 hover 树。
275
+ // 弹窗场景的裁剪问题由 useFloatToolbarPortal 负责把 toolbar 挂到正确的 popup host。
276
+ return (
277
+ getToolbarPopupContainer(triggerNode) ||
278
+ getPopupContainer?.(triggerNode) ||
279
+ triggerNode?.parentElement ||
280
+ document.body
281
+ );
282
+ },
283
+ [getPopupContainer],
284
+ );
285
+ const handleOpenChange: DropdownProps['onOpenChange'] = useCallback(
286
+ (nextOpen: boolean, info) => {
287
+ if (info.source === 'trigger' || nextOpen) {
288
+ // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
289
+ startTransition(() => {
290
+ setVisible(nextOpen);
291
+ });
292
+ onDropdownVisibleChange?.(nextOpen);
293
+ }
294
+ },
295
+ [onDropdownVisibleChange],
296
+ );
297
+ useEffect(() => {
298
+ return () => {
299
+ onDropdownVisibleChange?.(false);
300
+ };
301
+ }, [onDropdownVisibleChange]);
222
302
  const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]);
223
303
  useEffect(() => {
224
304
  let mounted = true;
@@ -422,7 +502,11 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
422
502
  return;
423
503
  }
424
504
 
425
- const extra = extraMenuItems.find((it) => it?.key === originalKey || it?.key === cleanKey);
505
+ const extra =
506
+ findExtraMenuItemByKey(extraMenuItems, originalKey) || findExtraMenuItemByKey(extraMenuItems, cleanKey);
507
+ if (extra?.disabled) {
508
+ return;
509
+ }
426
510
  if (extra?.onClick) {
427
511
  closeDropdown();
428
512
  extra.onClick();
@@ -795,7 +879,8 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
795
879
  // });
796
880
 
797
881
  if (commonExtras.length > 0) {
798
- items.push(...(commonExtras as MenuProps['items']));
882
+ // Antd Menu 会同时触发 item.onClick menu.onClick,这里统一交给 handleMenuClick 执行。
883
+ items.push(...(commonExtras.map(removeExtraMenuItemClickHandlers) as MenuProps['items']));
799
884
  }
800
885
 
801
886
  // 添加复制uid按钮
@@ -833,6 +918,9 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
833
918
 
834
919
  return (
835
920
  <Dropdown
921
+ getPopupContainer={resolvePopupContainer}
922
+ overlayClassName={TOOLBAR_DROPDOWN_OVERLAY_CLASS}
923
+ overlayStyle={{ width: 'max-content', minWidth: 'max-content' }}
836
924
  onOpenChange={handleOpenChange}
837
925
  open={visible}
838
926
  menu={{