@nocobase/flow-engine 2.1.0-alpha.21 → 2.1.0-alpha.23
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/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +2 -0
- package/lib/components/FlowModelRenderer.js +2 -0
- package/lib/components/dnd/index.d.ts +19 -1
- package/lib/components/dnd/index.js +239 -21
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +20 -1
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +4 -0
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +21 -8
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +2 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +100 -32
- 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/data-source/index.d.ts +73 -0
- package/lib/data-source/index.js +205 -1
- package/lib/flowContext.d.ts +2 -0
- package/lib/flowI18n.js +2 -1
- 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 +11 -9
- package/lib/models/flowModel.js +48 -9
- package/lib/provider.js +38 -23
- package/package.json +4 -4
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +6 -0
- package/src/components/__tests__/FlowModelRenderer.test.tsx +22 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/dnd/index.tsx +286 -26
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +25 -1
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +24 -5
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +94 -3
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +171 -2
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +2 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +112 -32
- package/src/components/subModel/index.ts +1 -0
- package/src/data-source/__tests__/index.test.ts +34 -1
- package/src/data-source/index.ts +252 -2
- package/src/flowContext.ts +2 -0
- package/src/flowI18n.ts +2 -1
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/flowModel.tsx +85 -23
- package/src/provider.tsx +41 -25
|
@@ -90,11 +90,21 @@ describe('FlowModelRenderer', () => {
|
|
|
90
90
|
|
|
91
91
|
test('should clear stale beforeRender state after unmount when reusing the same model', async () => {
|
|
92
92
|
const statefulEngine = new FlowEngine();
|
|
93
|
+
const onMountSpy = vi.fn();
|
|
94
|
+
const onUnmountSpy = vi.fn();
|
|
93
95
|
|
|
94
96
|
class StatefulModel extends FlowModel {
|
|
95
97
|
render(): any {
|
|
96
98
|
return <div>Stateful Content</div>;
|
|
97
99
|
}
|
|
100
|
+
|
|
101
|
+
protected onMount(): void {
|
|
102
|
+
onMountSpy();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
protected onUnmount(): void {
|
|
106
|
+
onUnmountSpy();
|
|
107
|
+
}
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
const statefulModel = new StatefulModel({
|
|
@@ -107,8 +117,14 @@ describe('FlowModelRenderer', () => {
|
|
|
107
117
|
await waitFor(() => {
|
|
108
118
|
expect(executorSpy).toHaveBeenCalledTimes(1);
|
|
109
119
|
});
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(onMountSpy).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
110
123
|
|
|
111
124
|
firstRender.unmount();
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(onUnmountSpy).toHaveBeenCalledTimes(1);
|
|
127
|
+
});
|
|
112
128
|
|
|
113
129
|
executorSpy.mockClear();
|
|
114
130
|
statefulModel.setStepParams('anyFlow', 'anyStep', { x: 1 });
|
|
@@ -119,6 +135,9 @@ describe('FlowModelRenderer', () => {
|
|
|
119
135
|
await waitFor(() => {
|
|
120
136
|
expect(executorSpy).toHaveBeenCalledTimes(1);
|
|
121
137
|
});
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(onMountSpy).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
122
141
|
const [target, eventName, inputArgs, options] = executorSpy.mock.calls[0];
|
|
123
142
|
expect(target).toBe(statefulModel);
|
|
124
143
|
expect(eventName).toBe('beforeRender');
|
|
@@ -126,5 +145,8 @@ describe('FlowModelRenderer', () => {
|
|
|
126
145
|
expect(options).toMatchObject({ useCache: true });
|
|
127
146
|
|
|
128
147
|
secondRender.unmount();
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(onUnmountSpy).toHaveBeenCalledTimes(2);
|
|
150
|
+
});
|
|
129
151
|
});
|
|
130
152
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
11
|
+
import { resolveOverlayAnchorTransform } from '../dnd';
|
|
12
|
+
|
|
13
|
+
describe('resolveOverlayAnchorTransform', () => {
|
|
14
|
+
it('should keep the original transform when anchor point is missing', () => {
|
|
15
|
+
const transform = { x: 24, y: 36, scaleX: 1, scaleY: 1 };
|
|
16
|
+
|
|
17
|
+
expect(
|
|
18
|
+
resolveOverlayAnchorTransform({
|
|
19
|
+
activeId: 'menu-item-1',
|
|
20
|
+
active: { id: 'menu-item-1' },
|
|
21
|
+
transform,
|
|
22
|
+
activeNodeRect: { top: 80, left: 120 },
|
|
23
|
+
dragAnchorPoint: null,
|
|
24
|
+
}),
|
|
25
|
+
).toEqual(transform);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should align the overlay origin to the pointer position when dragging from toolbar handle', () => {
|
|
29
|
+
expect(
|
|
30
|
+
resolveOverlayAnchorTransform({
|
|
31
|
+
activeId: 'menu-item-1',
|
|
32
|
+
active: { id: 'menu-item-1' },
|
|
33
|
+
transform: { x: 20, y: 30, scaleX: 1, scaleY: 1 },
|
|
34
|
+
activeNodeRect: { top: 200, left: 100 },
|
|
35
|
+
dragAnchorPoint: { x: 180, y: 260 },
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({
|
|
38
|
+
x: 100,
|
|
39
|
+
y: 90,
|
|
40
|
+
scaleX: 1,
|
|
41
|
+
scaleY: 1,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -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
|
|
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
|
|
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={
|
|
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
|
}}
|
|
@@ -78,7 +291,40 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
|
|
|
78
291
|
...restProps
|
|
79
292
|
}) => {
|
|
80
293
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
294
|
+
const [dragAnchorPoint, setDragAnchorPoint] = useState<ToolbarDragAnchorDetail['point']>(null);
|
|
81
295
|
const flowEngine = useFlowEngine();
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (typeof document === 'undefined') {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const handleToolbarDragAnchor = (event: Event) => {
|
|
303
|
+
const customEvent = event as CustomEvent<ToolbarDragAnchorDetail>;
|
|
304
|
+
setDragAnchorPoint(customEvent.detail?.point || null);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
document.addEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
|
|
308
|
+
return () => {
|
|
309
|
+
document.removeEventListener(TOOLBAR_DRAG_ANCHOR_EVENT, handleToolbarDragAnchor as EventListener);
|
|
310
|
+
};
|
|
311
|
+
}, []);
|
|
312
|
+
|
|
313
|
+
const overlayAnchorModifier = useCallback<Modifier>(
|
|
314
|
+
({ active, activeNodeRect, transform }) => {
|
|
315
|
+
const nextTransform: Transform = resolveOverlayAnchorTransform({
|
|
316
|
+
activeId,
|
|
317
|
+
active,
|
|
318
|
+
transform,
|
|
319
|
+
activeNodeRect,
|
|
320
|
+
dragAnchorPoint,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return nextTransform;
|
|
324
|
+
},
|
|
325
|
+
[activeId, dragAnchorPoint],
|
|
326
|
+
);
|
|
327
|
+
|
|
82
328
|
return (
|
|
83
329
|
<DndContext
|
|
84
330
|
onDragStart={(event) => {
|
|
@@ -87,6 +333,7 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
|
|
|
87
333
|
}}
|
|
88
334
|
onDragEnd={(event) => {
|
|
89
335
|
setActiveId(null);
|
|
336
|
+
setDragAnchorPoint(null);
|
|
90
337
|
// 如果没有 onDragEnd 回调,则默认调用 flowEngine 的 moveModel 方法
|
|
91
338
|
if (!onDragEnd) {
|
|
92
339
|
if (event.over) {
|
|
@@ -97,32 +344,45 @@ export const DndProvider: FC<DndContextProps & PersistOptions> = ({
|
|
|
97
344
|
onDragEnd(event);
|
|
98
345
|
}
|
|
99
346
|
}}
|
|
347
|
+
onDragCancel={(event) => {
|
|
348
|
+
setActiveId(null);
|
|
349
|
+
setDragAnchorPoint(null);
|
|
350
|
+
restProps.onDragCancel?.(event);
|
|
351
|
+
}}
|
|
100
352
|
{...restProps}
|
|
101
353
|
>
|
|
102
354
|
{children}
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
}}
|
|
355
|
+
{typeof document !== 'undefined'
|
|
356
|
+
? createPortal(
|
|
357
|
+
<DragOverlay
|
|
358
|
+
dropAnimation={null}
|
|
359
|
+
modifiers={[overlayAnchorModifier]}
|
|
360
|
+
zIndex={2000}
|
|
361
|
+
style={{ pointerEvents: 'none' }}
|
|
119
362
|
>
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
363
|
+
{activeId && (
|
|
364
|
+
<span
|
|
365
|
+
style={{
|
|
366
|
+
display: 'inline-flex',
|
|
367
|
+
alignItems: 'center',
|
|
368
|
+
whiteSpace: 'nowrap',
|
|
369
|
+
background: '#fff',
|
|
370
|
+
border: '1px solid #1890ff',
|
|
371
|
+
borderRadius: 4,
|
|
372
|
+
padding: '4px 12px',
|
|
373
|
+
color: '#1890ff',
|
|
374
|
+
pointerEvents: 'none',
|
|
375
|
+
// fontSize: 18,
|
|
376
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
{flowEngine.translate('Dragging')}
|
|
380
|
+
</span>
|
|
381
|
+
)}
|
|
382
|
+
</DragOverlay>,
|
|
383
|
+
document.body,
|
|
384
|
+
)
|
|
385
|
+
: null}
|
|
126
386
|
</DndContext>
|
|
127
387
|
);
|
|
128
388
|
};
|
|
@@ -27,6 +27,26 @@ import { useNiceDropdownMaxHeight } from '../../../../hooks';
|
|
|
27
27
|
import { SwitchWithTitle } from '../component/SwitchWithTitle';
|
|
28
28
|
import { SelectWithTitle } from '../component/SelectWithTitle';
|
|
29
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
|
+
|
|
30
50
|
// Type definitions for better type safety
|
|
31
51
|
interface StepInfo {
|
|
32
52
|
stepKey: string;
|
|
@@ -473,7 +493,11 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
473
493
|
return;
|
|
474
494
|
}
|
|
475
495
|
|
|
476
|
-
const extra =
|
|
496
|
+
const extra =
|
|
497
|
+
findExtraMenuItemByKey(extraMenuItems, originalKey) || findExtraMenuItemByKey(extraMenuItems, cleanKey);
|
|
498
|
+
if (extra?.disabled) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
477
501
|
if (extra?.onClick) {
|
|
478
502
|
closeDropdown();
|
|
479
503
|
extra.onClick();
|
|
@@ -27,9 +27,9 @@ import {
|
|
|
27
27
|
} from './useFloatToolbarPortal';
|
|
28
28
|
import { useFloatToolbarVisibility } from './useFloatToolbarVisibility';
|
|
29
29
|
|
|
30
|
-
const TOOLBAR_Z_INDEX = 999;
|
|
31
|
-
|
|
32
30
|
type ToolbarPosition = 'inside' | 'above' | 'below';
|
|
31
|
+
const TOOLBAR_ITEM_WIDTH = 19;
|
|
32
|
+
const DEFAULT_POPUP_BASE_Z_INDEX = 1000;
|
|
33
33
|
|
|
34
34
|
interface BaseFloatContextMenuProps {
|
|
35
35
|
children?: React.ReactNode;
|
|
@@ -64,6 +64,10 @@ interface BaseFloatContextMenuProps {
|
|
|
64
64
|
* Extra toolbar items to add to this context menu instance
|
|
65
65
|
*/
|
|
66
66
|
extraToolbarItems?: ToolbarItemConfig[];
|
|
67
|
+
/**
|
|
68
|
+
* @default true
|
|
69
|
+
*/
|
|
70
|
+
showDynamicFlowsEditor?: boolean;
|
|
67
71
|
/**
|
|
68
72
|
* @default 'inside'
|
|
69
73
|
*/
|
|
@@ -97,12 +101,14 @@ const toolbarContainerStyles = ({
|
|
|
97
101
|
showBackground,
|
|
98
102
|
showBorder,
|
|
99
103
|
ctx,
|
|
104
|
+
toolbarZIndex,
|
|
100
105
|
}: {
|
|
101
106
|
showBackground: boolean;
|
|
102
107
|
showBorder: boolean;
|
|
103
108
|
ctx: any;
|
|
109
|
+
toolbarZIndex: number;
|
|
104
110
|
}) => css`
|
|
105
|
-
z-index: ${
|
|
111
|
+
z-index: ${toolbarZIndex};
|
|
106
112
|
opacity: 0;
|
|
107
113
|
pointer-events: none;
|
|
108
114
|
overflow: visible;
|
|
@@ -294,6 +300,7 @@ const renderToolbarItems = (
|
|
|
294
300
|
flowEngine: FlowEngine,
|
|
295
301
|
settingsMenuLevel?: number,
|
|
296
302
|
extraToolbarItems?: ToolbarItemConfig[],
|
|
303
|
+
showDynamicFlowsEditor = true,
|
|
297
304
|
onSettingsMenuOpenChange?: (open: boolean) => void,
|
|
298
305
|
getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement,
|
|
299
306
|
) => {
|
|
@@ -304,6 +311,9 @@ const renderToolbarItems = (
|
|
|
304
311
|
|
|
305
312
|
return allToolbarItems
|
|
306
313
|
.filter((itemConfig: ToolbarItemConfig) => {
|
|
314
|
+
if (itemConfig.key === 'dynamic-flows-editor' && showDynamicFlowsEditor === false) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
307
317
|
return itemConfig.visible ? itemConfig.visible(model) : true;
|
|
308
318
|
})
|
|
309
319
|
.map((itemConfig: ToolbarItemConfig) => {
|
|
@@ -332,6 +342,7 @@ const buildToolbarContainerClassName = ({
|
|
|
332
342
|
showBackground,
|
|
333
343
|
showBorder,
|
|
334
344
|
ctx,
|
|
345
|
+
toolbarZIndex,
|
|
335
346
|
portalRenderSnapshot,
|
|
336
347
|
isToolbarVisible,
|
|
337
348
|
className,
|
|
@@ -339,12 +350,13 @@ const buildToolbarContainerClassName = ({
|
|
|
339
350
|
showBackground: boolean;
|
|
340
351
|
showBorder: boolean;
|
|
341
352
|
ctx: any;
|
|
353
|
+
toolbarZIndex: number;
|
|
342
354
|
portalRenderSnapshot: ToolbarPortalRenderSnapshot | null;
|
|
343
355
|
isToolbarVisible: boolean;
|
|
344
356
|
className?: string;
|
|
345
357
|
}) =>
|
|
346
358
|
[
|
|
347
|
-
toolbarContainerStyles({ showBackground, showBorder, ctx }),
|
|
359
|
+
toolbarContainerStyles({ showBackground, showBorder, ctx, toolbarZIndex }),
|
|
348
360
|
'nb-toolbar-portal',
|
|
349
361
|
portalRenderSnapshot?.positioningMode === 'absolute' ? 'nb-toolbar-portal-absolute' : 'nb-toolbar-portal-fixed',
|
|
350
362
|
isToolbarVisible ? 'nb-toolbar-visible' : '',
|
|
@@ -356,11 +368,13 @@ const buildToolbarContainerClassName = ({
|
|
|
356
368
|
const buildToolbarContainerStyle = (
|
|
357
369
|
portalRect: ToolbarPortalRect,
|
|
358
370
|
toolbarStyle?: React.CSSProperties,
|
|
371
|
+
toolbarItemCount = 0,
|
|
359
372
|
): React.CSSProperties => ({
|
|
360
373
|
top: `${portalRect.top}px`,
|
|
361
374
|
left: `${portalRect.left}px`,
|
|
362
375
|
width: `${portalRect.width}px`,
|
|
363
376
|
height: `${portalRect.height}px`,
|
|
377
|
+
minWidth: toolbarItemCount ? `${TOOLBAR_ITEM_WIDTH * toolbarItemCount}px` : undefined,
|
|
364
378
|
...omitToolbarPortalInsetStyle(toolbarStyle),
|
|
365
379
|
});
|
|
366
380
|
|
|
@@ -514,6 +528,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
514
528
|
showDragHandle = false,
|
|
515
529
|
settingsMenuLevel,
|
|
516
530
|
extraToolbarItems,
|
|
531
|
+
showDynamicFlowsEditor = true,
|
|
517
532
|
toolbarStyle,
|
|
518
533
|
toolbarPosition = 'inside',
|
|
519
534
|
}: ModelProvidedProps) => {
|
|
@@ -575,6 +590,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
575
590
|
flowEngine,
|
|
576
591
|
settingsMenuLevel,
|
|
577
592
|
extraToolbarItems,
|
|
593
|
+
showDynamicFlowsEditor,
|
|
578
594
|
handleSettingsMenuOpenChange,
|
|
579
595
|
getPopupContainer,
|
|
580
596
|
)
|
|
@@ -588,6 +604,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
588
604
|
settingsMenuLevel,
|
|
589
605
|
showCopyUidButton,
|
|
590
606
|
showDeleteButton,
|
|
607
|
+
showDynamicFlowsEditor,
|
|
591
608
|
],
|
|
592
609
|
);
|
|
593
610
|
|
|
@@ -625,15 +642,17 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
625
642
|
return <>{children}</>;
|
|
626
643
|
}
|
|
627
644
|
|
|
645
|
+
const toolbarZIndex = (model.context.themeToken?.zIndexPopupBase || DEFAULT_POPUP_BASE_Z_INDEX) + 1;
|
|
628
646
|
const toolbarContainerClassName = buildToolbarContainerClassName({
|
|
629
647
|
showBackground,
|
|
630
648
|
showBorder,
|
|
631
649
|
ctx: model.context,
|
|
650
|
+
toolbarZIndex,
|
|
632
651
|
portalRenderSnapshot,
|
|
633
652
|
isToolbarVisible,
|
|
634
653
|
className,
|
|
635
654
|
});
|
|
636
|
-
const toolbarContainerStyle = buildToolbarContainerStyle(portalRect, toolbarStyle);
|
|
655
|
+
const toolbarContainerStyle = buildToolbarContainerStyle(portalRect, toolbarStyle, toolbarItems.length);
|
|
637
656
|
|
|
638
657
|
const toolbarNode = shouldRenderToolbar ? (
|
|
639
658
|
<div
|