@nocobase/flow-engine 2.0.27 → 2.0.29
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/FlowModelRenderer.d.ts +1 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +48 -9
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +19 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +332 -296
- 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 +247 -0
- package/package.json +4 -4
- package/src/components/FlowModelRenderer.tsx +3 -1
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +17 -7
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +62 -9
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +457 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +95 -0
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +547 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +281 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
-
import { render, cleanup, waitFor } from '@testing-library/react';
|
|
12
|
+
import { render, cleanup, fireEvent, waitFor } from '@testing-library/react';
|
|
13
13
|
import { App, ConfigProvider } from 'antd';
|
|
14
14
|
import { FlowEngine } from '../../flowEngine';
|
|
15
15
|
import { FlowModel, ModelRenderMode } from '../../models/flowModel';
|
|
@@ -94,6 +94,16 @@ const clickDeleteFromLastDropdown = async () => {
|
|
|
94
94
|
menu.onClick?.({ key: 'delete' });
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
const getHost = (element: HTMLElement) => element.closest('[data-has-float-menu="true"]') as HTMLDivElement;
|
|
98
|
+
|
|
99
|
+
const hoverHostAndClickDelete = async (element: HTMLElement) => {
|
|
100
|
+
const host = getHost(element);
|
|
101
|
+
if (host) {
|
|
102
|
+
fireEvent.mouseEnter(host);
|
|
103
|
+
}
|
|
104
|
+
await clickDeleteFromLastDropdown();
|
|
105
|
+
};
|
|
106
|
+
|
|
97
107
|
// ---------------- Tests ----------------
|
|
98
108
|
describe('Delete problematic model via FlowSettings menu', () => {
|
|
99
109
|
beforeEach(() => {
|
|
@@ -120,7 +130,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
120
130
|
// satisfy FlowsFloatContextMenu styles
|
|
121
131
|
model.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
122
132
|
|
|
123
|
-
render(
|
|
133
|
+
const { findByTestId } = render(
|
|
124
134
|
<ConfigProvider>
|
|
125
135
|
<App>
|
|
126
136
|
<FlowEngineProvider engine={engine}>
|
|
@@ -130,7 +140,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
130
140
|
</ConfigProvider>,
|
|
131
141
|
);
|
|
132
142
|
|
|
133
|
-
await
|
|
143
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
134
144
|
expect(engine.getModel(model.uid)).toBeUndefined();
|
|
135
145
|
});
|
|
136
146
|
|
|
@@ -163,7 +173,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
163
173
|
parent.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
164
174
|
child.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
165
175
|
|
|
166
|
-
render(
|
|
176
|
+
const { findByTestId } = render(
|
|
167
177
|
<ConfigProvider>
|
|
168
178
|
<App>
|
|
169
179
|
<FlowEngineProvider engine={engine}>
|
|
@@ -173,7 +183,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
173
183
|
</ConfigProvider>,
|
|
174
184
|
);
|
|
175
185
|
|
|
176
|
-
await
|
|
186
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
177
187
|
expect(engine.getModel(child.uid)).toBeUndefined();
|
|
178
188
|
const remain = (parent.subModels as any).items || [];
|
|
179
189
|
expect(remain.find((m: FlowModel) => m.uid === child.uid)).toBeUndefined();
|
|
@@ -208,7 +218,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
208
218
|
parent.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
209
219
|
child.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
210
220
|
|
|
211
|
-
render(
|
|
221
|
+
const { findByTestId } = render(
|
|
212
222
|
<ConfigProvider>
|
|
213
223
|
<App>
|
|
214
224
|
<FlowEngineProvider engine={engine}>
|
|
@@ -218,7 +228,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
218
228
|
</ConfigProvider>,
|
|
219
229
|
);
|
|
220
230
|
|
|
221
|
-
await
|
|
231
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
222
232
|
expect(engine.getModel(child.uid)).toBeUndefined();
|
|
223
233
|
const remain = (parent.subModels as any).cells || [];
|
|
224
234
|
expect(remain.find((m: FlowModel) => m.uid === child.uid)).toBeUndefined();
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ExclamationCircleOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
|
11
|
+
import { css } from '@emotion/css';
|
|
11
12
|
import type { DropdownProps, MenuProps } from 'antd';
|
|
12
13
|
import { App, Dropdown, Modal, Tooltip, theme } from 'antd';
|
|
13
14
|
import React, { startTransition, useCallback, useEffect, useMemo, useState, FC } from 'react';
|
|
@@ -188,15 +189,42 @@ interface DefaultSettingsIconProps {
|
|
|
188
189
|
showCopyUidButton?: boolean;
|
|
189
190
|
menuLevels?: number; // Menu levels: 1=current model only (default), 2=include sub-models
|
|
190
191
|
flattenSubMenus?: boolean; // Whether to flatten sub-menus: false=group by model (default), true=flatten all
|
|
192
|
+
onDropdownVisibleChange?: (open: boolean) => void;
|
|
193
|
+
getPopupContainer?: DropdownProps['getPopupContainer'];
|
|
191
194
|
[key: string]: any; // Allow additional props
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
const TOOLBAR_ICONS_SELECTOR = '.nb-toolbar-container-icons';
|
|
198
|
+
const TOOLBAR_CONTAINER_SELECTOR = '.nb-toolbar-container';
|
|
199
|
+
const TOOLBAR_DROPDOWN_OVERLAY_CLASS = css`
|
|
200
|
+
width: max-content;
|
|
201
|
+
min-width: max-content;
|
|
202
|
+
|
|
203
|
+
.ant-dropdown-menu {
|
|
204
|
+
width: max-content;
|
|
205
|
+
min-width: max-content;
|
|
206
|
+
}
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
const getToolbarPopupContainer = (triggerNode?: HTMLElement | null) => {
|
|
210
|
+
if (!triggerNode) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
(triggerNode.closest(TOOLBAR_ICONS_SELECTOR) as HTMLElement | null) ||
|
|
216
|
+
(triggerNode.closest(TOOLBAR_CONTAINER_SELECTOR) as HTMLElement | null)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
194
220
|
export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
195
221
|
model,
|
|
196
222
|
showDeleteButton = true,
|
|
197
223
|
showCopyUidButton = true,
|
|
198
224
|
menuLevels = 1, // 默认一级菜单
|
|
199
225
|
flattenSubMenus = true,
|
|
226
|
+
onDropdownVisibleChange,
|
|
227
|
+
getPopupContainer,
|
|
200
228
|
}) => {
|
|
201
229
|
const { message } = App.useApp();
|
|
202
230
|
const t = useMemo(() => getT(model), [model]);
|
|
@@ -210,15 +238,37 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
210
238
|
const [isLoading, setIsLoading] = useState(true);
|
|
211
239
|
const closeDropdown = useCallback(() => {
|
|
212
240
|
setVisible(false);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
onDropdownVisibleChange?.(false);
|
|
242
|
+
}, [onDropdownVisibleChange]);
|
|
243
|
+
const resolvePopupContainer = useCallback<NonNullable<DropdownProps['getPopupContainer']>>(
|
|
244
|
+
(triggerNode) => {
|
|
245
|
+
// 优先挂到工具栏自身容器,避免 modal / drawer 中鼠标从图标移动到菜单时先离开工具栏树。
|
|
246
|
+
return (
|
|
247
|
+
getToolbarPopupContainer(triggerNode) ||
|
|
248
|
+
getPopupContainer?.(triggerNode) ||
|
|
249
|
+
triggerNode?.parentElement ||
|
|
250
|
+
document.body
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
[getPopupContainer],
|
|
254
|
+
);
|
|
255
|
+
const handleOpenChange: DropdownProps['onOpenChange'] = useCallback(
|
|
256
|
+
(nextOpen: boolean, info) => {
|
|
257
|
+
if (info.source === 'trigger' || nextOpen) {
|
|
258
|
+
// 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
|
|
259
|
+
startTransition(() => {
|
|
260
|
+
setVisible(nextOpen);
|
|
261
|
+
});
|
|
262
|
+
onDropdownVisibleChange?.(nextOpen);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
[onDropdownVisibleChange],
|
|
266
|
+
);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
return () => {
|
|
269
|
+
onDropdownVisibleChange?.(false);
|
|
270
|
+
};
|
|
271
|
+
}, [onDropdownVisibleChange]);
|
|
222
272
|
const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]);
|
|
223
273
|
useEffect(() => {
|
|
224
274
|
let mounted = true;
|
|
@@ -833,6 +883,9 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
833
883
|
|
|
834
884
|
return (
|
|
835
885
|
<Dropdown
|
|
886
|
+
getPopupContainer={resolvePopupContainer}
|
|
887
|
+
overlayClassName={TOOLBAR_DROPDOWN_OVERLAY_CLASS}
|
|
888
|
+
overlayStyle={{ width: 'max-content', minWidth: 'max-content' }}
|
|
836
889
|
onOpenChange={handleOpenChange}
|
|
837
890
|
open={visible}
|
|
838
891
|
menu={{
|