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

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 (194) 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/FormItem.d.ts +6 -0
  10. package/lib/components/FormItem.js +11 -3
  11. package/lib/components/MobilePopup.js +6 -5
  12. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  13. package/lib/components/dnd/gridDragPlanner.js +613 -21
  14. package/lib/components/dnd/index.d.ts +31 -2
  15. package/lib/components/dnd/index.js +244 -23
  16. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  17. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  18. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  19. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +68 -10
  20. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  21. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  22. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  23. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  24. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  25. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  26. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  27. package/lib/components/subModel/AddSubModelButton.js +27 -1
  28. package/lib/components/subModel/LazyDropdown.js +96 -39
  29. package/lib/components/subModel/index.d.ts +1 -0
  30. package/lib/components/subModel/index.js +19 -0
  31. package/lib/components/subModel/utils.d.ts +1 -1
  32. package/lib/components/subModel/utils.js +9 -3
  33. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  34. package/lib/components/variables/VariableHybridInput.js +499 -0
  35. package/lib/components/variables/index.d.ts +2 -0
  36. package/lib/components/variables/index.js +3 -0
  37. package/lib/data-source/index.d.ts +75 -0
  38. package/lib/data-source/index.js +247 -5
  39. package/lib/executor/FlowExecutor.js +32 -9
  40. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  41. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  42. package/lib/flow-registry/index.d.ts +1 -0
  43. package/lib/flow-registry/index.js +3 -1
  44. package/lib/flowContext.d.ts +3 -0
  45. package/lib/flowContext.js +43 -1
  46. package/lib/flowEngine.d.ts +151 -1
  47. package/lib/flowEngine.js +389 -15
  48. package/lib/flowI18n.js +2 -1
  49. package/lib/flowSettings.d.ts +14 -6
  50. package/lib/flowSettings.js +34 -6
  51. package/lib/index.d.ts +2 -0
  52. package/lib/index.js +7 -0
  53. package/lib/lazy-helper.d.ts +14 -0
  54. package/lib/lazy-helper.js +71 -0
  55. package/lib/locale/en-US.json +1 -0
  56. package/lib/locale/index.d.ts +2 -0
  57. package/lib/locale/zh-CN.json +1 -0
  58. package/lib/models/DisplayItemModel.d.ts +1 -1
  59. package/lib/models/EditableItemModel.d.ts +1 -1
  60. package/lib/models/FilterableItemModel.d.ts +1 -1
  61. package/lib/models/flowModel.d.ts +13 -10
  62. package/lib/models/flowModel.js +78 -18
  63. package/lib/provider.js +38 -23
  64. package/lib/reactive/observer.js +46 -16
  65. package/lib/runjs-context/registry.d.ts +1 -1
  66. package/lib/runjs-context/setup.js +20 -12
  67. package/lib/runjs-context/snippets/index.js +13 -2
  68. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  69. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  70. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  71. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  72. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  73. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  74. package/lib/types.d.ts +50 -2
  75. package/lib/types.js +1 -0
  76. package/lib/utils/createCollectionContextMeta.js +6 -2
  77. package/lib/utils/index.d.ts +3 -2
  78. package/lib/utils/index.js +7 -0
  79. package/lib/utils/parsePathnameToViewParams.js +1 -1
  80. package/lib/utils/randomId.d.ts +39 -0
  81. package/lib/utils/randomId.js +45 -0
  82. package/lib/utils/runjsTemplateCompat.js +1 -1
  83. package/lib/utils/runjsValue.js +41 -11
  84. package/lib/utils/schema-utils.d.ts +7 -1
  85. package/lib/utils/schema-utils.js +19 -0
  86. package/lib/views/FlowView.d.ts +7 -1
  87. package/lib/views/FlowView.js +11 -1
  88. package/lib/views/PageComponent.js +8 -6
  89. package/lib/views/ViewNavigation.js +6 -2
  90. package/lib/views/runViewBeforeClose.d.ts +10 -0
  91. package/lib/views/runViewBeforeClose.js +45 -0
  92. package/lib/views/useDialog.d.ts +2 -1
  93. package/lib/views/useDialog.js +20 -3
  94. package/lib/views/useDrawer.d.ts +2 -1
  95. package/lib/views/useDrawer.js +20 -3
  96. package/lib/views/usePage.d.ts +5 -11
  97. package/lib/views/usePage.js +302 -144
  98. package/package.json +6 -5
  99. package/src/JSRunner.ts +68 -4
  100. package/src/ViewScopedFlowEngine.ts +4 -0
  101. package/src/__tests__/JSRunner.test.ts +27 -1
  102. package/src/__tests__/flow-engine.test.ts +166 -0
  103. package/src/__tests__/flowContext.test.ts +82 -1
  104. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  105. package/src/__tests__/flowSettings.test.ts +94 -15
  106. package/src/__tests__/objectVariable.test.ts +24 -0
  107. package/src/__tests__/provider.test.tsx +24 -2
  108. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  109. package/src/__tests__/runjsContext.test.ts +16 -0
  110. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  111. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  112. package/src/__tests__/runjsSnippets.test.ts +21 -0
  113. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  114. package/src/components/FieldModelRenderer.tsx +2 -1
  115. package/src/components/FlowModelRenderer.tsx +18 -6
  116. package/src/components/FormItem.tsx +7 -1
  117. package/src/components/MobilePopup.tsx +4 -2
  118. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  119. package/src/components/__tests__/FormItem.test.tsx +25 -0
  120. package/src/components/__tests__/dnd.test.ts +44 -0
  121. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  122. package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
  123. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  124. package/src/components/dnd/gridDragPlanner.ts +758 -19
  125. package/src/components/dnd/index.tsx +305 -28
  126. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  127. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +88 -10
  128. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  129. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  130. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +189 -3
  131. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  132. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  133. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  134. package/src/components/subModel/AddSubModelButton.tsx +32 -2
  135. package/src/components/subModel/LazyDropdown.tsx +107 -43
  136. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +319 -36
  137. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  138. package/src/components/subModel/index.ts +1 -0
  139. package/src/components/subModel/utils.ts +7 -1
  140. package/src/components/variables/VariableHybridInput.tsx +531 -0
  141. package/src/components/variables/index.ts +2 -0
  142. package/src/data-source/__tests__/collection.test.ts +41 -2
  143. package/src/data-source/__tests__/index.test.ts +68 -1
  144. package/src/data-source/index.ts +304 -6
  145. package/src/executor/FlowExecutor.ts +35 -10
  146. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  147. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  148. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  149. package/src/flow-registry/index.ts +1 -0
  150. package/src/flowContext.ts +47 -3
  151. package/src/flowEngine.ts +445 -11
  152. package/src/flowI18n.ts +2 -1
  153. package/src/flowSettings.ts +40 -6
  154. package/src/index.ts +2 -0
  155. package/src/lazy-helper.tsx +57 -0
  156. package/src/locale/en-US.json +1 -0
  157. package/src/locale/zh-CN.json +1 -0
  158. package/src/models/DisplayItemModel.tsx +1 -1
  159. package/src/models/EditableItemModel.tsx +1 -1
  160. package/src/models/FilterableItemModel.tsx +1 -1
  161. package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
  162. package/src/models/__tests__/flowModel.test.ts +47 -3
  163. package/src/models/flowModel.tsx +119 -33
  164. package/src/provider.tsx +41 -25
  165. package/src/reactive/__tests__/observer.test.tsx +82 -0
  166. package/src/reactive/observer.tsx +87 -25
  167. package/src/runjs-context/registry.ts +1 -1
  168. package/src/runjs-context/setup.ts +22 -12
  169. package/src/runjs-context/snippets/index.ts +12 -1
  170. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  171. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  172. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  173. package/src/types.ts +62 -0
  174. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  175. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  176. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  177. package/src/utils/__tests__/utils.test.ts +62 -0
  178. package/src/utils/createCollectionContextMeta.ts +6 -2
  179. package/src/utils/index.ts +5 -1
  180. package/src/utils/parsePathnameToViewParams.ts +2 -2
  181. package/src/utils/randomId.ts +48 -0
  182. package/src/utils/runjsTemplateCompat.ts +1 -1
  183. package/src/utils/runjsValue.ts +50 -11
  184. package/src/utils/schema-utils.ts +30 -1
  185. package/src/views/FlowView.tsx +22 -2
  186. package/src/views/PageComponent.tsx +7 -4
  187. package/src/views/ViewNavigation.ts +6 -2
  188. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  189. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  190. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  191. package/src/views/runViewBeforeClose.ts +19 -0
  192. package/src/views/useDialog.tsx +25 -3
  193. package/src/views/useDrawer.tsx +25 -3
  194. package/src/views/usePage.tsx +365 -179
@@ -0,0 +1,361 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { useCallback, useEffect, useRef, useState } from 'react';
11
+ import type { MouseEvent as ReactMouseEvent, RefObject } from 'react';
12
+ import { TOOLBAR_DRAG_ACTIVITY_EVENT } from '../../../dnd';
13
+
14
+ const TOOLBAR_HIDE_DELAY = 180;
15
+ const CHILD_FLOAT_MENU_ACTIVITY_EVENT = 'nb-float-menu-child-activity';
16
+
17
+ interface UseFloatToolbarVisibilityOptions {
18
+ modelUid: string;
19
+ containerRef: RefObject<HTMLDivElement>;
20
+ toolbarContainerRef: RefObject<HTMLDivElement>;
21
+ updatePortalRect: () => void;
22
+ schedulePortalRectUpdate: () => void;
23
+ }
24
+
25
+ interface UseFloatToolbarVisibilityResult {
26
+ isToolbarVisible: boolean;
27
+ shouldRenderToolbar: boolean;
28
+ handleSettingsMenuOpenChange: (open: boolean) => void;
29
+ handleChildHover: (e: ReactMouseEvent) => void;
30
+ handleHostMouseEnter: () => void;
31
+ handleHostMouseLeave: (e: ReactMouseEvent<HTMLDivElement>) => void;
32
+ handleToolbarMouseEnter: () => void;
33
+ handleToolbarMouseLeave: (e: ReactMouseEvent<HTMLDivElement>) => void;
34
+ handleResizeDragStart: () => void;
35
+ handleResizeDragEnd: () => void;
36
+ }
37
+
38
+ const isNodeWithin = (target: EventTarget | null, container: HTMLElement | null): boolean => {
39
+ return target instanceof Node && !!container?.contains(target);
40
+ };
41
+
42
+ const getToolbarModelUidFromTarget = (target: EventTarget | null): string | null => {
43
+ if (!(target instanceof Element)) {
44
+ return null;
45
+ }
46
+
47
+ return target.closest('.nb-toolbar-container[data-model-uid]')?.getAttribute('data-model-uid') || null;
48
+ };
49
+
50
+ const isNodeWithinDescendantFloatToolbar = (
51
+ target: EventTarget | null,
52
+ container: HTMLElement | null,
53
+ currentModelUid: string,
54
+ ): boolean => {
55
+ const targetModelUid = getToolbarModelUidFromTarget(target);
56
+ if (!container || !targetModelUid || targetModelUid === currentModelUid) {
57
+ return false;
58
+ }
59
+
60
+ return Array.from(
61
+ container.querySelectorAll<HTMLElement>('[data-has-float-menu="true"][data-float-menu-model-uid]'),
62
+ ).some(
63
+ (hostElement) =>
64
+ hostElement !== container && hostElement.getAttribute('data-float-menu-model-uid') === targetModelUid,
65
+ );
66
+ };
67
+
68
+ export const useFloatToolbarVisibility = ({
69
+ modelUid,
70
+ containerRef,
71
+ toolbarContainerRef,
72
+ updatePortalRect,
73
+ schedulePortalRectUpdate,
74
+ }: UseFloatToolbarVisibilityOptions): UseFloatToolbarVisibilityResult => {
75
+ const [hideMenu, setHideMenu] = useState(false);
76
+ const [isHostHovered, setIsHostHovered] = useState(false);
77
+ const [isToolbarHovered, setIsToolbarHovered] = useState(false);
78
+ const [isDraggingToolbar, setIsDraggingToolbar] = useState(false);
79
+ const [isDraggingToolbarItem, setIsDraggingToolbarItem] = useState(false);
80
+ const [isToolbarPinned, setIsToolbarPinned] = useState(false);
81
+ const [isHidePending, setIsHidePending] = useState(false);
82
+ const [activeChildToolbarIds, setActiveChildToolbarIds] = useState<string[]>([]);
83
+ const hideToolbarTimerRef = useRef<number | null>(null);
84
+ const reportedChildActivityToAncestorsRef = useRef(false);
85
+ const isHostHoveredRef = useRef(false);
86
+ const isToolbarHoveredRef = useRef(false);
87
+ const isDraggingToolbarRef = useRef(false);
88
+ const isDraggingToolbarItemRef = useRef(false);
89
+ const isToolbarPinnedRef = useRef(false);
90
+
91
+ const setHostHovered = useCallback((value: boolean) => {
92
+ isHostHoveredRef.current = value;
93
+ setIsHostHovered(value);
94
+ }, []);
95
+
96
+ const setToolbarHovered = useCallback((value: boolean) => {
97
+ isToolbarHoveredRef.current = value;
98
+ setIsToolbarHovered(value);
99
+ }, []);
100
+
101
+ const setDraggingToolbar = useCallback((value: boolean) => {
102
+ isDraggingToolbarRef.current = value;
103
+ setIsDraggingToolbar(value);
104
+ }, []);
105
+
106
+ const setDraggingToolbarItem = useCallback((value: boolean) => {
107
+ isDraggingToolbarItemRef.current = value;
108
+ setIsDraggingToolbarItem(value);
109
+ }, []);
110
+
111
+ const setToolbarPinned = useCallback((value: boolean) => {
112
+ isToolbarPinnedRef.current = value;
113
+ setIsToolbarPinned(value);
114
+ }, []);
115
+
116
+ const hasActiveChildToolbar = activeChildToolbarIds.length > 0;
117
+ const isToolbarVisible =
118
+ !hideMenu &&
119
+ !hasActiveChildToolbar &&
120
+ (isHostHovered || isToolbarHovered || isDraggingToolbar || isDraggingToolbarItem || isToolbarPinned);
121
+ const shouldRenderToolbar = isToolbarVisible || isToolbarPinned || isDraggingToolbar || isDraggingToolbarItem;
122
+ const isToolbarInteractionActive =
123
+ isHostHovered || isToolbarHovered || isDraggingToolbar || isDraggingToolbarItem || isToolbarPinned || isHidePending;
124
+
125
+ const clearHideToolbarTimer = useCallback(() => {
126
+ if (hideToolbarTimerRef.current !== null) {
127
+ window.clearTimeout(hideToolbarTimerRef.current);
128
+ hideToolbarTimerRef.current = null;
129
+ }
130
+ setIsHidePending(false);
131
+ }, []);
132
+
133
+ const scheduleHideToolbar = useCallback(() => {
134
+ clearHideToolbarTimer();
135
+ setIsHidePending(true);
136
+ hideToolbarTimerRef.current = window.setTimeout(() => {
137
+ hideToolbarTimerRef.current = null;
138
+ setIsHidePending(false);
139
+ if (isDraggingToolbarRef.current || isDraggingToolbarItemRef.current || isToolbarPinnedRef.current) {
140
+ return;
141
+ }
142
+ setHostHovered(false);
143
+ setToolbarHovered(false);
144
+ }, TOOLBAR_HIDE_DELAY);
145
+ }, [clearHideToolbarTimer, setHostHovered, setToolbarHovered]);
146
+
147
+ const handleSettingsMenuOpenChange = useCallback(
148
+ (open: boolean) => {
149
+ setToolbarPinned(open);
150
+ },
151
+ [setToolbarPinned],
152
+ );
153
+
154
+ useEffect(() => {
155
+ const hostElement = containerRef.current;
156
+ if (!hostElement) {
157
+ return;
158
+ }
159
+
160
+ const handleChildToolbarActivity = (event: Event) => {
161
+ const customEvent = event as CustomEvent<{ active?: boolean; modelUid?: string }>;
162
+ if (!(customEvent.target instanceof HTMLElement) || customEvent.target === hostElement) {
163
+ return;
164
+ }
165
+
166
+ const childModelUid = customEvent.detail?.modelUid;
167
+ if (!childModelUid) {
168
+ return;
169
+ }
170
+
171
+ setActiveChildToolbarIds((prevIds) => {
172
+ return customEvent.detail?.active
173
+ ? prevIds.includes(childModelUid)
174
+ ? prevIds
175
+ : [...prevIds, childModelUid]
176
+ : prevIds.filter((id) => id !== childModelUid);
177
+ });
178
+ };
179
+
180
+ hostElement.addEventListener(CHILD_FLOAT_MENU_ACTIVITY_EVENT, handleChildToolbarActivity as EventListener);
181
+ return () => {
182
+ hostElement.removeEventListener(CHILD_FLOAT_MENU_ACTIVITY_EVENT, handleChildToolbarActivity as EventListener);
183
+ };
184
+ }, [containerRef]);
185
+
186
+ useEffect(() => {
187
+ const hostElement = containerRef.current;
188
+ const ownerDocument = hostElement?.ownerDocument;
189
+ if (!ownerDocument) {
190
+ return;
191
+ }
192
+
193
+ const handleToolbarDragActivity = (event: Event) => {
194
+ const customEvent = event as CustomEvent<{ active?: boolean; modelUid?: string }>;
195
+ if (customEvent.detail?.modelUid !== modelUid) {
196
+ return;
197
+ }
198
+
199
+ if (customEvent.detail?.active) {
200
+ clearHideToolbarTimer();
201
+ setDraggingToolbarItem(true);
202
+ return;
203
+ }
204
+
205
+ setDraggingToolbarItem(false);
206
+ if (isHostHoveredRef.current || isToolbarHoveredRef.current || isToolbarPinnedRef.current) {
207
+ clearHideToolbarTimer();
208
+ return;
209
+ }
210
+
211
+ scheduleHideToolbar();
212
+ };
213
+
214
+ ownerDocument.addEventListener(TOOLBAR_DRAG_ACTIVITY_EVENT, handleToolbarDragActivity as EventListener);
215
+ return () => {
216
+ ownerDocument.removeEventListener(TOOLBAR_DRAG_ACTIVITY_EVENT, handleToolbarDragActivity as EventListener);
217
+ };
218
+ }, [clearHideToolbarTimer, containerRef, modelUid, scheduleHideToolbar, setDraggingToolbarItem]);
219
+
220
+ useEffect(() => {
221
+ const hostElement = containerRef.current;
222
+ if (!hostElement || reportedChildActivityToAncestorsRef.current === isToolbarInteractionActive) {
223
+ return;
224
+ }
225
+
226
+ reportedChildActivityToAncestorsRef.current = isToolbarInteractionActive;
227
+ hostElement.dispatchEvent(
228
+ new CustomEvent(CHILD_FLOAT_MENU_ACTIVITY_EVENT, {
229
+ bubbles: true,
230
+ detail: { active: isToolbarInteractionActive, modelUid },
231
+ }),
232
+ );
233
+ }, [containerRef, isToolbarInteractionActive, modelUid]);
234
+
235
+ useEffect(() => {
236
+ const hostElement = containerRef.current;
237
+
238
+ return () => {
239
+ if (hostElement && reportedChildActivityToAncestorsRef.current) {
240
+ hostElement.dispatchEvent(
241
+ new CustomEvent(CHILD_FLOAT_MENU_ACTIVITY_EVENT, {
242
+ bubbles: true,
243
+ detail: { active: false, modelUid },
244
+ }),
245
+ );
246
+ reportedChildActivityToAncestorsRef.current = false;
247
+ }
248
+ clearHideToolbarTimer();
249
+ };
250
+ }, [clearHideToolbarTimer, containerRef, modelUid]);
251
+
252
+ useEffect(() => {
253
+ if (isToolbarPinned) {
254
+ clearHideToolbarTimer();
255
+ updatePortalRect();
256
+ }
257
+ }, [clearHideToolbarTimer, isToolbarPinned, updatePortalRect]);
258
+
259
+ const handleChildHover = useCallback(
260
+ (e: ReactMouseEvent) => {
261
+ const target = e.target as HTMLElement;
262
+ const childWithMenu = target.closest('[data-has-float-menu]');
263
+ const isCurrentHostTarget = !childWithMenu || childWithMenu === containerRef.current;
264
+
265
+ if (isCurrentHostTarget) {
266
+ clearHideToolbarTimer();
267
+ setHostHovered(true);
268
+ }
269
+
270
+ setHideMenu(!!childWithMenu && childWithMenu !== containerRef.current);
271
+ },
272
+ [clearHideToolbarTimer, containerRef, setHostHovered],
273
+ );
274
+
275
+ const handleHostMouseEnter = useCallback(() => {
276
+ clearHideToolbarTimer();
277
+ setHideMenu(false);
278
+ updatePortalRect();
279
+ setHostHovered(true);
280
+ }, [clearHideToolbarTimer, setHostHovered, updatePortalRect]);
281
+
282
+ const handleHostMouseLeave = useCallback(
283
+ (e: ReactMouseEvent<HTMLDivElement>) => {
284
+ if (isToolbarPinnedRef.current) {
285
+ setHostHovered(false);
286
+ return;
287
+ }
288
+ if (isNodeWithin(e.relatedTarget, toolbarContainerRef.current)) {
289
+ clearHideToolbarTimer();
290
+ setHostHovered(false);
291
+ setToolbarHovered(true);
292
+ return;
293
+ }
294
+ if (isNodeWithinDescendantFloatToolbar(e.relatedTarget, containerRef.current, modelUid)) {
295
+ clearHideToolbarTimer();
296
+ setHideMenu(false);
297
+ setHostHovered(true);
298
+ return;
299
+ }
300
+ scheduleHideToolbar();
301
+ },
302
+ [
303
+ clearHideToolbarTimer,
304
+ containerRef,
305
+ modelUid,
306
+ scheduleHideToolbar,
307
+ setHostHovered,
308
+ setToolbarHovered,
309
+ toolbarContainerRef,
310
+ ],
311
+ );
312
+
313
+ const handleToolbarMouseEnter = useCallback(() => {
314
+ clearHideToolbarTimer();
315
+ updatePortalRect();
316
+ setHostHovered(false);
317
+ setToolbarHovered(true);
318
+ }, [clearHideToolbarTimer, setHostHovered, setToolbarHovered, updatePortalRect]);
319
+
320
+ const handleToolbarMouseLeave = useCallback(
321
+ (e: ReactMouseEvent<HTMLDivElement>) => {
322
+ if (isToolbarPinnedRef.current || isDraggingToolbarItemRef.current) {
323
+ clearHideToolbarTimer();
324
+ setToolbarHovered(false);
325
+ return;
326
+ }
327
+ setToolbarHovered(false);
328
+ if (isNodeWithin(e.relatedTarget, containerRef.current)) {
329
+ clearHideToolbarTimer();
330
+ setHostHovered(true);
331
+ return;
332
+ }
333
+ scheduleHideToolbar();
334
+ },
335
+ [clearHideToolbarTimer, containerRef, scheduleHideToolbar, setHostHovered, setToolbarHovered],
336
+ );
337
+
338
+ const handleResizeDragStart = useCallback(() => {
339
+ updatePortalRect();
340
+ setDraggingToolbar(true);
341
+ schedulePortalRectUpdate();
342
+ }, [schedulePortalRectUpdate, setDraggingToolbar, updatePortalRect]);
343
+
344
+ const handleResizeDragEnd = useCallback(() => {
345
+ setDraggingToolbar(false);
346
+ schedulePortalRectUpdate();
347
+ }, [schedulePortalRectUpdate, setDraggingToolbar]);
348
+
349
+ return {
350
+ isToolbarVisible,
351
+ shouldRenderToolbar,
352
+ handleSettingsMenuOpenChange,
353
+ handleChildHover,
354
+ handleHostMouseEnter,
355
+ handleHostMouseLeave,
356
+ handleToolbarMouseEnter,
357
+ handleToolbarMouseLeave,
358
+ handleResizeDragStart,
359
+ handleResizeDragEnd,
360
+ };
361
+ };
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { Switch } from 'antd';
11
11
  import _ from 'lodash';
12
- import React, { useMemo } from 'react';
12
+ import React, { useEffect, useMemo } from 'react';
13
13
  import { FlowModelContext } from '../../flowContext';
14
14
  import { FlowModel } from '../../models';
15
15
  import { CreateModelOptions, ModelConstructor } from '../../types';
@@ -542,6 +542,22 @@ const AddSubModelButtonCore = function AddSubModelButton({
542
542
  [model, subModelKey, subModelType],
543
543
  );
544
544
 
545
+ React.useEffect(() => {
546
+ const handleSubModelChanged = () => {
547
+ setRefreshTick((x) => x + 1);
548
+ };
549
+
550
+ model.emitter?.on('onSubModelAdded', handleSubModelChanged);
551
+ model.emitter?.on('onSubModelRemoved', handleSubModelChanged);
552
+ model.emitter?.on('onSubModelReplaced', handleSubModelChanged);
553
+
554
+ return () => {
555
+ model.emitter?.off('onSubModelAdded', handleSubModelChanged);
556
+ model.emitter?.off('onSubModelRemoved', handleSubModelChanged);
557
+ model.emitter?.off('onSubModelReplaced', handleSubModelChanged);
558
+ };
559
+ }, [model]);
560
+
545
561
  // 点击处理逻辑
546
562
  const onClick = async (info: any) => {
547
563
  const clickedItem = info.originalItem || info;
@@ -594,7 +610,7 @@ const AddSubModelButtonCore = function AddSubModelButton({
594
610
  let addedModel: FlowModel | undefined;
595
611
 
596
612
  try {
597
- addedModel = model.flowEngine.createModel({
613
+ addedModel = await model.flowEngine.createModelAsync({
598
614
  ..._.cloneDeep(createOpts),
599
615
  parentId: model.uid,
600
616
  subKey: subModelKey,
@@ -651,6 +667,20 @@ const AddSubModelButtonCore = function AddSubModelButton({
651
667
  [finalItems, model, subModelKey, subModelType],
652
668
  );
653
669
 
670
+ useEffect(() => {
671
+ const handleSubModelChange = () => {
672
+ setRefreshTick((x) => x + 1);
673
+ };
674
+
675
+ model.emitter.on('onSubModelAdded', handleSubModelChange);
676
+ model.emitter.on('onSubModelRemoved', handleSubModelChange);
677
+
678
+ return () => {
679
+ model.emitter.off('onSubModelAdded', handleSubModelChange);
680
+ model.emitter.off('onSubModelRemoved', handleSubModelChange);
681
+ };
682
+ }, [model]);
683
+
654
684
  return (
655
685
  <LazyDropdown
656
686
  menu={{
@@ -358,6 +358,32 @@ const SearchInputWithAutoFocus: FC<InputProps & { visible: boolean }> = (props)
358
358
 
359
359
  const getKeyPath = (path: string[], key: string) => [...path, key].join('/');
360
360
 
361
+ const normalizeOpenKeys = (nextOpenKeys: string[]) => {
362
+ const latestKey = nextOpenKeys[nextOpenKeys.length - 1];
363
+
364
+ if (!latestKey) {
365
+ return [];
366
+ }
367
+
368
+ return nextOpenKeys.filter((key) => latestKey === key || latestKey.startsWith(`${key}/`));
369
+ };
370
+
371
+ const getLabelSearchText = (label: React.ReactNode): string => {
372
+ if (label === null || label === undefined || typeof label === 'boolean') {
373
+ return '';
374
+ }
375
+ if (typeof label === 'string' || typeof label === 'number') {
376
+ return String(label);
377
+ }
378
+ if (Array.isArray(label)) {
379
+ return label.map(getLabelSearchText).join(' ');
380
+ }
381
+ if (React.isValidElement(label)) {
382
+ return getLabelSearchText(label.props.children);
383
+ }
384
+ return '';
385
+ };
386
+
361
387
  const createSearchItem = (
362
388
  item: Item,
363
389
  searchKey: string,
@@ -406,6 +432,11 @@ const createEmptyItem = (itemKey: string, t: (key: string) => string) => ({
406
432
  disabled: true,
407
433
  });
408
434
 
435
+ const KEEP_OPEN_LABEL_STYLE: React.CSSProperties = {
436
+ display: 'block',
437
+ width: '100%',
438
+ };
439
+
409
440
  // ==================== Main Component ====================
410
441
 
411
442
  // 短暂保持打开状态的注册表(用于跨父节点快速重建时的恢复)
@@ -435,6 +466,19 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
435
466
  const { searchValues, isSearching, updateSearchValue } = useMenuSearch();
436
467
  const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
437
468
  useSubmenuStyles(menuVisible, dropdownMaxHeight);
469
+ const handleMenuOpenChange = useCallback(
470
+ (nextOpenKeys: string[]) => {
471
+ if (!nextOpenKeys.length && shouldPreventClose()) {
472
+ dropdownMenuProps.onOpenChange?.(Array.from(openKeys));
473
+ return;
474
+ }
475
+
476
+ const normalized = normalizeOpenKeys(nextOpenKeys);
477
+ setOpenKeys(new Set(normalized));
478
+ dropdownMenuProps.onOpenChange?.(normalized);
479
+ },
480
+ [dropdownMenuProps, openKeys, shouldPreventClose],
481
+ );
438
482
 
439
483
  // 在挂载时,若存在 persistKey 且仍在持久期内,则尝试恢复打开状态
440
484
  useEffect(() => {
@@ -460,6 +504,12 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
460
504
  };
461
505
  }, [persistKey, menuVisible]);
462
506
 
507
+ useEffect(() => {
508
+ if (!menuVisible) {
509
+ setOpenKeys(new Set());
510
+ }
511
+ }, [menuVisible]);
512
+
463
513
  // 加载根 items,支持同步/异步函数
464
514
  useEffect(() => {
465
515
  const loadRootItems = async () => {
@@ -498,15 +548,10 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
498
548
  const filteredChildren = currentSearchValue
499
549
  ? (function deepFilter(items: Item[]): Item[] {
500
550
  const searchText = currentSearchValue.toLowerCase();
501
- const tryString = (v: any) => {
502
- if (!v) return '';
503
- return typeof v === 'string' ? v : String(v);
504
- };
505
551
  return items
506
552
  .map((child) => {
507
- const labelStr = tryString(child.label).toLowerCase();
508
- const selfMatch =
509
- labelStr.includes(searchText) || (child.key && String(child.key).toLowerCase().includes(searchText));
553
+ const labelStr = getLabelSearchText(child.label).toLowerCase();
554
+ const selfMatch = labelStr.includes(searchText);
510
555
  if (child.type === 'group' && Array.isArray(child.children)) {
511
556
  const nested = deepFilter(child.children);
512
557
  if (selfMatch || nested.length > 0) {
@@ -588,56 +633,73 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
588
633
  return { type: 'divider', key: keyPath };
589
634
  }
590
635
 
636
+ const label = typeof item.label === 'string' ? t(item.label) : item.label;
637
+
591
638
  // 非 group 的“子菜单”也支持本层级搜索:当 item.searchable = true 且存在 children 时
592
639
  if (item.searchable && children) {
593
640
  return {
594
- key: item.key,
595
- label: typeof item.label === 'string' ? t(item.label) : item.label,
641
+ key: keyPath,
642
+ label,
596
643
  onClick: (info: any) => {},
597
- onMouseEnter: () => {
598
- setOpenKeys((prev) => {
599
- if (prev.has(keyPath)) return prev;
600
- const next = new Set(prev);
601
- next.add(keyPath);
602
- return next;
603
- });
604
- },
605
644
  children: buildSearchChildren(children, item, keyPath, path, menuVisible, resolveItems),
606
645
  };
607
646
  }
608
647
 
648
+ const itemShouldKeepOpen = !children && (item.keepDropdownOpen ?? keepDropdownOpen ?? false);
649
+ const handleLeafClick = (info: any) => {
650
+ if (children) {
651
+ return;
652
+ }
653
+
654
+ if (itemShouldKeepOpen) {
655
+ requestKeepOpen();
656
+ }
657
+
658
+ const extendedInfo: ExtendedMenuInfo = {
659
+ ...info,
660
+ key: info?.key ?? keyPath,
661
+ keyPath: info?.keyPath ?? [keyPath],
662
+ item: info?.item || item,
663
+ originalItem: item,
664
+ keepDropdownOpen: itemShouldKeepOpen,
665
+ };
666
+
667
+ menu.onClick?.(extendedInfo);
668
+ };
669
+
609
670
  return {
610
671
  key: keyPath,
611
- label: typeof item.label === 'string' ? t(item.label) : item.label,
672
+ label: itemShouldKeepOpen ? (
673
+ <div
674
+ style={KEEP_OPEN_LABEL_STYLE}
675
+ onMouseDown={(event) => {
676
+ event.stopPropagation();
677
+ requestKeepOpen();
678
+ }}
679
+ onClick={(event) => {
680
+ event.stopPropagation();
681
+ handleLeafClick({
682
+ key: keyPath,
683
+ keyPath: [keyPath],
684
+ item,
685
+ domEvent: event,
686
+ });
687
+ }}
688
+ >
689
+ {label}
690
+ </div>
691
+ ) : (
692
+ label
693
+ ),
612
694
  onClick: (info: any) => {
613
- if (children) {
695
+ if (!itemShouldKeepOpen) handleLeafClick(info);
696
+ },
697
+ onMouseDown: () => {
698
+ if (!itemShouldKeepOpen) {
614
699
  return;
615
700
  }
616
701
 
617
- // 检查是否应该保持下拉菜单打开
618
- const itemShouldKeepOpen = item.keepDropdownOpen ?? keepDropdownOpen ?? false;
619
-
620
- // 如果需要保持菜单打开,请求保持打开状态
621
- if (itemShouldKeepOpen) {
622
- requestKeepOpen();
623
- }
624
-
625
- const extendedInfo: ExtendedMenuInfo = {
626
- ...info,
627
- item: info.item || item,
628
- originalItem: item,
629
- keepDropdownOpen: itemShouldKeepOpen,
630
- };
631
-
632
- menu.onClick?.(extendedInfo);
633
- },
634
- onMouseEnter: () => {
635
- setOpenKeys((prev) => {
636
- if (prev.has(keyPath)) return prev;
637
- const next = new Set(prev);
638
- next.add(keyPath);
639
- return next;
640
- });
702
+ requestKeepOpen();
641
703
  },
642
704
  children:
643
705
  children && children.length > 0
@@ -684,8 +746,10 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
684
746
  placement="bottomLeft"
685
747
  menu={{
686
748
  ...dropdownMenuProps,
749
+ openKeys: Array.from(openKeys),
687
750
  items: items,
688
751
  onClick: () => {},
752
+ onOpenChange: handleMenuOpenChange,
689
753
  style: {
690
754
  maxHeight: dropdownMaxHeight,
691
755
  overflowY: 'auto',