@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.
- package/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +613 -21
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +68 -10
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/LazyDropdown.js +96 -39
- package/lib/components/subModel/index.d.ts +1 -0
- package/lib/components/subModel/index.js +19 -0
- package/lib/components/subModel/utils.d.ts +1 -1
- package/lib/components/subModel/utils.js +9 -3
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +75 -0
- package/lib/data-source/index.js +247 -5
- package/lib/executor/FlowExecutor.js +32 -9
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +3 -0
- package/lib/flowContext.js +43 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +13 -10
- package/lib/models/flowModel.js +78 -18
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +20 -12
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
- package/lib/scheduler/ModelOperationScheduler.js +3 -2
- package/lib/types.d.ts +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.js +6 -2
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +20 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +20 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +302 -144
- package/package.json +6 -5
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +82 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +16 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +758 -19
- package/src/components/dnd/index.tsx +305 -28
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +88 -10
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +189 -3
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/LazyDropdown.tsx +107 -43
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +319 -36
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +7 -1
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +68 -1
- package/src/data-source/index.ts +304 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +47 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/__tests__/flowModel.test.ts +47 -3
- package/src/models/flowModel.tsx +119 -33
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +22 -12
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/scheduler/ModelOperationScheduler.ts +14 -3
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +5 -1
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/randomId.ts +48 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +6 -2
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +25 -3
- package/src/views/useDrawer.tsx +25 -3
- 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.
|
|
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 =
|
|
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:
|
|
595
|
-
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:
|
|
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 (
|
|
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',
|