@nocobase/flow-engine 2.1.0-beta.21 → 2.1.0-beta.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__/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
package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { FlowEngineProvider } from '../../../../../provider';
|
|
|
17
17
|
import { FieldModelRenderer } from '../../../../FieldModelRenderer';
|
|
18
18
|
import { FlowModelRenderer } from '../../../../FlowModelRenderer';
|
|
19
19
|
import { FlowsFloatContextMenu } from '../FlowsFloatContextMenu';
|
|
20
|
+
import { TOOLBAR_DRAG_ACTIVITY_EVENT } from '../../../../dnd';
|
|
20
21
|
|
|
21
22
|
const mockColorTextTertiary = '#8c8c8c';
|
|
22
23
|
|
|
@@ -151,17 +152,36 @@ const setupDrawerPopup = () => {
|
|
|
151
152
|
return { drawerWrapper, drawerContent };
|
|
152
153
|
};
|
|
153
154
|
|
|
155
|
+
const setupOverflowPopup = () => {
|
|
156
|
+
const appContainer = createAppContainer();
|
|
157
|
+
const popupRoot = document.createElement('div');
|
|
158
|
+
popupRoot.className = 'ant-menu-submenu-popup';
|
|
159
|
+
popupRoot.style.zIndex = '1000';
|
|
160
|
+
appContainer.appendChild(popupRoot);
|
|
161
|
+
mockRect(appContainer, { top: 40, left: 60, width: 1200, height: 800 });
|
|
162
|
+
mockRect(popupRoot, { top: 96, left: 420, width: 260, height: 240 });
|
|
163
|
+
return { appContainer, popupRoot };
|
|
164
|
+
};
|
|
165
|
+
|
|
154
166
|
const getHost = (element: HTMLElement) => element.closest('[data-has-float-menu="true"]') as HTMLDivElement;
|
|
155
167
|
const queryOverlay = (container: HTMLElement, uid: string) =>
|
|
156
168
|
container.querySelector(`[data-model-uid="${uid}"]`) as HTMLDivElement | null;
|
|
157
169
|
|
|
158
|
-
const createModel = (engine: FlowEngine, uid: string) => {
|
|
170
|
+
const createModel = (engine: FlowEngine, uid: string, themeToken?: Record<string, number | undefined>) => {
|
|
159
171
|
const model = new FlowModel({ uid, flowEngine: engine });
|
|
160
|
-
model.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
172
|
+
model.context.defineProperty('themeToken', { value: { borderRadiusLG: 8, ...themeToken } });
|
|
161
173
|
model.render = vi.fn().mockReturnValue(<div data-testid={`${uid}-content`}>{uid}</div>);
|
|
162
174
|
return model;
|
|
163
175
|
};
|
|
164
176
|
|
|
177
|
+
const ToolbarDragItem = ({ model }: { model: FlowModel }) => {
|
|
178
|
+
return (
|
|
179
|
+
<button type="button" aria-label="toolbar-drag">
|
|
180
|
+
drag
|
|
181
|
+
</button>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
165
185
|
describe('FlowsFloatContextMenu', () => {
|
|
166
186
|
const originalResizeObserver = globalThis.ResizeObserver;
|
|
167
187
|
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
@@ -314,6 +334,49 @@ describe('FlowsFloatContextMenu', () => {
|
|
|
314
334
|
});
|
|
315
335
|
});
|
|
316
336
|
|
|
337
|
+
it('renders overflow popup toolbar above popup roots while keeping dropdown popup bound to local icons container', async () => {
|
|
338
|
+
const engine = new FlowEngine();
|
|
339
|
+
await engine.flowSettings.forceEnable();
|
|
340
|
+
const model = createModel(engine, 'overflow-popup-model', { zIndexPopupBase: 1000 });
|
|
341
|
+
const { appContainer, popupRoot } = setupOverflowPopup();
|
|
342
|
+
|
|
343
|
+
const { findByTestId } = renderWithProviders(
|
|
344
|
+
engine,
|
|
345
|
+
<FlowModelRenderer model={model} showFlowSettings={{ toolbarPosition: 'above' }} />,
|
|
346
|
+
{ container: popupRoot },
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const content = await findByTestId('overflow-popup-model-content');
|
|
350
|
+
const host = getHost(content);
|
|
351
|
+
mockRect(host, { top: 128, left: 436, width: 180, height: 40 });
|
|
352
|
+
|
|
353
|
+
expect(getComputedStyle(popupRoot).zIndex).toBe('1000');
|
|
354
|
+
expect(appContainer.querySelector('[data-model-uid="overflow-popup-model"]')).toBeNull();
|
|
355
|
+
|
|
356
|
+
fireEvent.mouseEnter(host);
|
|
357
|
+
|
|
358
|
+
const overlay = await waitFor(() => {
|
|
359
|
+
const nextOverlay = appContainer.querySelector(
|
|
360
|
+
'[data-model-uid="overflow-popup-model"]',
|
|
361
|
+
) as HTMLDivElement | null;
|
|
362
|
+
expect(nextOverlay).toBeTruthy();
|
|
363
|
+
return nextOverlay as HTMLDivElement;
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await waitFor(() => {
|
|
367
|
+
expect(within(overlay).getByLabelText('flows-settings')).toBeTruthy();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(overlay.className).toContain('nb-toolbar-visible');
|
|
372
|
+
expect(getComputedStyle(overlay).zIndex).toBe('1001');
|
|
373
|
+
expect(overlay.parentElement).toBe(popupRoot);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const dropdown = within(overlay).getByTestId('dropdown');
|
|
377
|
+
expect(dropdown.getAttribute('data-popup-container')).toContain('nb-toolbar-container-icons');
|
|
378
|
+
});
|
|
379
|
+
|
|
317
380
|
it('portals field toolbar to the nearest popup root and treats inset values as rect adjustments', async () => {
|
|
318
381
|
const engine = new FlowEngine();
|
|
319
382
|
await engine.flowSettings.forceEnable();
|
|
@@ -398,6 +461,112 @@ describe('FlowsFloatContextMenu', () => {
|
|
|
398
461
|
});
|
|
399
462
|
});
|
|
400
463
|
|
|
464
|
+
it('falls back to popup base 1000 when themeToken.zIndexPopupBase is missing', async () => {
|
|
465
|
+
const engine = new FlowEngine();
|
|
466
|
+
await engine.flowSettings.forceEnable();
|
|
467
|
+
const model = createModel(engine, 'fallback-zindex-model');
|
|
468
|
+
const appContainer = createAppContainer();
|
|
469
|
+
mockRect(appContainer, { top: 20, left: 40, width: 1200, height: 800 });
|
|
470
|
+
|
|
471
|
+
const { getByTestId } = renderWithProviders(
|
|
472
|
+
engine,
|
|
473
|
+
<FlowsFloatContextMenu model={model}>
|
|
474
|
+
<div data-testid="fallback-content">content</div>
|
|
475
|
+
</FlowsFloatContextMenu>,
|
|
476
|
+
{ container: appContainer },
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const host = getHost(getByTestId('fallback-content'));
|
|
480
|
+
mockRect(host, { top: 56, left: 84, width: 160, height: 48 });
|
|
481
|
+
|
|
482
|
+
fireEvent.mouseEnter(host);
|
|
483
|
+
|
|
484
|
+
const overlay = await waitFor(() => {
|
|
485
|
+
const nextOverlay = appContainer.querySelector(
|
|
486
|
+
'[data-model-uid="fallback-zindex-model"]',
|
|
487
|
+
) as HTMLDivElement | null;
|
|
488
|
+
expect(nextOverlay).toBeTruthy();
|
|
489
|
+
return nextOverlay as HTMLDivElement;
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
await waitFor(() => {
|
|
493
|
+
expect(within(overlay).getByLabelText('flows-settings')).toBeTruthy();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await waitFor(() => {
|
|
497
|
+
expect(overlay.className).toContain('nb-toolbar-visible');
|
|
498
|
+
expect(getComputedStyle(overlay).zIndex).toBe('1001');
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('keeps toolbar visible while a toolbar drag item is active', async () => {
|
|
503
|
+
const engine = new FlowEngine();
|
|
504
|
+
await engine.flowSettings.forceEnable();
|
|
505
|
+
const model = createModel(engine, 'drag-toolbar-model');
|
|
506
|
+
const appContainer = createAppContainer();
|
|
507
|
+
mockRect(appContainer, { top: 20, left: 40, width: 1200, height: 800 });
|
|
508
|
+
|
|
509
|
+
const { getByTestId } = renderWithProviders(
|
|
510
|
+
engine,
|
|
511
|
+
<FlowsFloatContextMenu
|
|
512
|
+
model={model}
|
|
513
|
+
extraToolbarItems={[
|
|
514
|
+
{
|
|
515
|
+
key: 'toolbar-drag',
|
|
516
|
+
component: ToolbarDragItem,
|
|
517
|
+
sort: 100,
|
|
518
|
+
},
|
|
519
|
+
]}
|
|
520
|
+
>
|
|
521
|
+
<div data-testid="drag-toolbar-content">content</div>
|
|
522
|
+
</FlowsFloatContextMenu>,
|
|
523
|
+
{ container: appContainer },
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const host = getHost(getByTestId('drag-toolbar-content'));
|
|
527
|
+
mockRect(host, { top: 56, left: 84, width: 160, height: 48 });
|
|
528
|
+
|
|
529
|
+
fireEvent.mouseEnter(host);
|
|
530
|
+
|
|
531
|
+
const overlay = await waitFor(() => {
|
|
532
|
+
const nextOverlay = appContainer.querySelector('[data-model-uid="drag-toolbar-model"]') as HTMLDivElement | null;
|
|
533
|
+
expect(nextOverlay).toBeTruthy();
|
|
534
|
+
return nextOverlay as HTMLDivElement;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const icons = overlay.querySelector('.nb-toolbar-container-icons') as HTMLDivElement;
|
|
538
|
+
|
|
539
|
+
await waitFor(() => {
|
|
540
|
+
expect(within(overlay).getByLabelText('toolbar-drag')).toBeTruthy();
|
|
541
|
+
expect(overlay.className).toContain('nb-toolbar-visible');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
fireEvent.mouseLeave(host, { relatedTarget: icons });
|
|
545
|
+
fireEvent.mouseEnter(icons, { relatedTarget: host });
|
|
546
|
+
|
|
547
|
+
const dragButton = within(overlay).getByLabelText('toolbar-drag');
|
|
548
|
+
dragButton.ownerDocument.dispatchEvent(
|
|
549
|
+
new CustomEvent(TOOLBAR_DRAG_ACTIVITY_EVENT, {
|
|
550
|
+
detail: { active: true, modelUid: model.uid },
|
|
551
|
+
}),
|
|
552
|
+
);
|
|
553
|
+
fireEvent.mouseLeave(icons, { relatedTarget: document.createElement('div') });
|
|
554
|
+
|
|
555
|
+
await waitFor(() => {
|
|
556
|
+
expect(queryOverlay(appContainer, 'drag-toolbar-model')?.className).toContain('nb-toolbar-visible');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
dragButton.ownerDocument.dispatchEvent(
|
|
560
|
+
new CustomEvent(TOOLBAR_DRAG_ACTIVITY_EVENT, {
|
|
561
|
+
detail: { active: false, modelUid: model.uid },
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
await waitFor(() => {
|
|
566
|
+
expect(queryOverlay(appContainer, 'drag-toolbar-model')).toBeNull();
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
401
570
|
it('hides parent toolbar when hovering a nested child host', async () => {
|
|
402
571
|
const engine = new FlowEngine();
|
|
403
572
|
await engine.flowSettings.forceEnable();
|
|
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
11
11
|
import type { CSSProperties, RefObject } from 'react';
|
|
12
12
|
|
|
13
13
|
const APP_CONTAINER_SELECTOR = '#nocobase-app-container';
|
|
14
|
+
const MENU_SUBMENU_POPUP_SELECTOR = '.ant-menu-submenu-popup';
|
|
14
15
|
const DRAWER_CONTENT_WRAPPER_SELECTOR = '.ant-drawer-content-wrapper';
|
|
15
16
|
const DRAWER_CONTENT_SELECTOR = '.ant-drawer-content';
|
|
16
17
|
const DRAWER_ROOT_SELECTOR = '.ant-drawer-root';
|
|
@@ -77,6 +78,7 @@ const createAbsolutePortalHostConfig = (element: HTMLElement): ToolbarPortalHost
|
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
const popupPortalHostResolvers: Array<(hostEl: HTMLElement | null) => HTMLElement | null> = [
|
|
81
|
+
(hostEl) => getClosestElement(hostEl, MENU_SUBMENU_POPUP_SELECTOR),
|
|
80
82
|
(hostEl) => getClosestElement(hostEl, DRAWER_CONTENT_WRAPPER_SELECTOR),
|
|
81
83
|
(hostEl) => getClosestElement(hostEl, MODAL_WRAP_SELECTOR),
|
|
82
84
|
(hostEl) => getClosestElement(hostEl, MODAL_SELECTOR),
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
11
|
import type { MouseEvent as ReactMouseEvent, RefObject } from 'react';
|
|
12
|
+
import { TOOLBAR_DRAG_ACTIVITY_EVENT } from '../../../dnd';
|
|
12
13
|
|
|
13
14
|
const TOOLBAR_HIDE_DELAY = 180;
|
|
14
15
|
const CHILD_FLOAT_MENU_ACTIVITY_EVENT = 'nb-float-menu-child-activity';
|
|
@@ -75,18 +76,51 @@ export const useFloatToolbarVisibility = ({
|
|
|
75
76
|
const [isHostHovered, setIsHostHovered] = useState(false);
|
|
76
77
|
const [isToolbarHovered, setIsToolbarHovered] = useState(false);
|
|
77
78
|
const [isDraggingToolbar, setIsDraggingToolbar] = useState(false);
|
|
79
|
+
const [isDraggingToolbarItem, setIsDraggingToolbarItem] = useState(false);
|
|
78
80
|
const [isToolbarPinned, setIsToolbarPinned] = useState(false);
|
|
79
81
|
const [isHidePending, setIsHidePending] = useState(false);
|
|
80
82
|
const [activeChildToolbarIds, setActiveChildToolbarIds] = useState<string[]>([]);
|
|
81
83
|
const hideToolbarTimerRef = useRef<number | null>(null);
|
|
82
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
|
+
}, []);
|
|
83
115
|
|
|
84
116
|
const hasActiveChildToolbar = activeChildToolbarIds.length > 0;
|
|
85
117
|
const isToolbarVisible =
|
|
86
|
-
!hideMenu &&
|
|
87
|
-
|
|
118
|
+
!hideMenu &&
|
|
119
|
+
!hasActiveChildToolbar &&
|
|
120
|
+
(isHostHovered || isToolbarHovered || isDraggingToolbar || isDraggingToolbarItem || isToolbarPinned);
|
|
121
|
+
const shouldRenderToolbar = isToolbarVisible || isToolbarPinned || isDraggingToolbar || isDraggingToolbarItem;
|
|
88
122
|
const isToolbarInteractionActive =
|
|
89
|
-
isHostHovered || isToolbarHovered || isDraggingToolbar || isToolbarPinned || isHidePending;
|
|
123
|
+
isHostHovered || isToolbarHovered || isDraggingToolbar || isDraggingToolbarItem || isToolbarPinned || isHidePending;
|
|
90
124
|
|
|
91
125
|
const clearHideToolbarTimer = useCallback(() => {
|
|
92
126
|
if (hideToolbarTimerRef.current !== null) {
|
|
@@ -102,17 +136,20 @@ export const useFloatToolbarVisibility = ({
|
|
|
102
136
|
hideToolbarTimerRef.current = window.setTimeout(() => {
|
|
103
137
|
hideToolbarTimerRef.current = null;
|
|
104
138
|
setIsHidePending(false);
|
|
105
|
-
if (
|
|
139
|
+
if (isDraggingToolbarRef.current || isDraggingToolbarItemRef.current || isToolbarPinnedRef.current) {
|
|
106
140
|
return;
|
|
107
141
|
}
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
setHostHovered(false);
|
|
143
|
+
setToolbarHovered(false);
|
|
110
144
|
}, TOOLBAR_HIDE_DELAY);
|
|
111
|
-
}, [clearHideToolbarTimer,
|
|
145
|
+
}, [clearHideToolbarTimer, setHostHovered, setToolbarHovered]);
|
|
112
146
|
|
|
113
|
-
const handleSettingsMenuOpenChange = useCallback(
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
const handleSettingsMenuOpenChange = useCallback(
|
|
148
|
+
(open: boolean) => {
|
|
149
|
+
setToolbarPinned(open);
|
|
150
|
+
},
|
|
151
|
+
[setToolbarPinned],
|
|
152
|
+
);
|
|
116
153
|
|
|
117
154
|
useEffect(() => {
|
|
118
155
|
const hostElement = containerRef.current;
|
|
@@ -146,6 +183,40 @@ export const useFloatToolbarVisibility = ({
|
|
|
146
183
|
};
|
|
147
184
|
}, [containerRef]);
|
|
148
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
|
+
|
|
149
220
|
useEffect(() => {
|
|
150
221
|
const hostElement = containerRef.current;
|
|
151
222
|
if (!hostElement || reportedChildActivityToAncestorsRef.current === isToolbarInteractionActive) {
|
|
@@ -193,78 +264,87 @@ export const useFloatToolbarVisibility = ({
|
|
|
193
264
|
|
|
194
265
|
if (isCurrentHostTarget) {
|
|
195
266
|
clearHideToolbarTimer();
|
|
196
|
-
|
|
267
|
+
setHostHovered(true);
|
|
197
268
|
}
|
|
198
269
|
|
|
199
270
|
setHideMenu(!!childWithMenu && childWithMenu !== containerRef.current);
|
|
200
271
|
},
|
|
201
|
-
[clearHideToolbarTimer, containerRef],
|
|
272
|
+
[clearHideToolbarTimer, containerRef, setHostHovered],
|
|
202
273
|
);
|
|
203
274
|
|
|
204
275
|
const handleHostMouseEnter = useCallback(() => {
|
|
205
276
|
clearHideToolbarTimer();
|
|
206
277
|
setHideMenu(false);
|
|
207
278
|
updatePortalRect();
|
|
208
|
-
|
|
209
|
-
}, [clearHideToolbarTimer, updatePortalRect]);
|
|
279
|
+
setHostHovered(true);
|
|
280
|
+
}, [clearHideToolbarTimer, setHostHovered, updatePortalRect]);
|
|
210
281
|
|
|
211
282
|
const handleHostMouseLeave = useCallback(
|
|
212
283
|
(e: ReactMouseEvent<HTMLDivElement>) => {
|
|
213
|
-
if (
|
|
214
|
-
|
|
284
|
+
if (isToolbarPinnedRef.current) {
|
|
285
|
+
setHostHovered(false);
|
|
215
286
|
return;
|
|
216
287
|
}
|
|
217
288
|
if (isNodeWithin(e.relatedTarget, toolbarContainerRef.current)) {
|
|
218
289
|
clearHideToolbarTimer();
|
|
219
|
-
|
|
220
|
-
|
|
290
|
+
setHostHovered(false);
|
|
291
|
+
setToolbarHovered(true);
|
|
221
292
|
return;
|
|
222
293
|
}
|
|
223
294
|
if (isNodeWithinDescendantFloatToolbar(e.relatedTarget, containerRef.current, modelUid)) {
|
|
224
295
|
clearHideToolbarTimer();
|
|
225
296
|
setHideMenu(false);
|
|
226
|
-
|
|
297
|
+
setHostHovered(true);
|
|
227
298
|
return;
|
|
228
299
|
}
|
|
229
300
|
scheduleHideToolbar();
|
|
230
301
|
},
|
|
231
|
-
[
|
|
302
|
+
[
|
|
303
|
+
clearHideToolbarTimer,
|
|
304
|
+
containerRef,
|
|
305
|
+
modelUid,
|
|
306
|
+
scheduleHideToolbar,
|
|
307
|
+
setHostHovered,
|
|
308
|
+
setToolbarHovered,
|
|
309
|
+
toolbarContainerRef,
|
|
310
|
+
],
|
|
232
311
|
);
|
|
233
312
|
|
|
234
313
|
const handleToolbarMouseEnter = useCallback(() => {
|
|
235
314
|
clearHideToolbarTimer();
|
|
236
315
|
updatePortalRect();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}, [clearHideToolbarTimer, updatePortalRect]);
|
|
316
|
+
setHostHovered(false);
|
|
317
|
+
setToolbarHovered(true);
|
|
318
|
+
}, [clearHideToolbarTimer, setHostHovered, setToolbarHovered, updatePortalRect]);
|
|
240
319
|
|
|
241
320
|
const handleToolbarMouseLeave = useCallback(
|
|
242
321
|
(e: ReactMouseEvent<HTMLDivElement>) => {
|
|
243
|
-
if (
|
|
244
|
-
|
|
322
|
+
if (isToolbarPinnedRef.current || isDraggingToolbarItemRef.current) {
|
|
323
|
+
clearHideToolbarTimer();
|
|
324
|
+
setToolbarHovered(false);
|
|
245
325
|
return;
|
|
246
326
|
}
|
|
247
|
-
|
|
327
|
+
setToolbarHovered(false);
|
|
248
328
|
if (isNodeWithin(e.relatedTarget, containerRef.current)) {
|
|
249
329
|
clearHideToolbarTimer();
|
|
250
|
-
|
|
330
|
+
setHostHovered(true);
|
|
251
331
|
return;
|
|
252
332
|
}
|
|
253
333
|
scheduleHideToolbar();
|
|
254
334
|
},
|
|
255
|
-
[clearHideToolbarTimer, containerRef,
|
|
335
|
+
[clearHideToolbarTimer, containerRef, scheduleHideToolbar, setHostHovered, setToolbarHovered],
|
|
256
336
|
);
|
|
257
337
|
|
|
258
338
|
const handleResizeDragStart = useCallback(() => {
|
|
259
339
|
updatePortalRect();
|
|
260
|
-
|
|
340
|
+
setDraggingToolbar(true);
|
|
261
341
|
schedulePortalRectUpdate();
|
|
262
|
-
}, [schedulePortalRectUpdate, updatePortalRect]);
|
|
342
|
+
}, [schedulePortalRectUpdate, setDraggingToolbar, updatePortalRect]);
|
|
263
343
|
|
|
264
344
|
const handleResizeDragEnd = useCallback(() => {
|
|
265
|
-
|
|
345
|
+
setDraggingToolbar(false);
|
|
266
346
|
schedulePortalRectUpdate();
|
|
267
|
-
}, [schedulePortalRectUpdate]);
|
|
347
|
+
}, [schedulePortalRectUpdate, setDraggingToolbar]);
|
|
268
348
|
|
|
269
349
|
return {
|
|
270
350
|
isToolbarVisible,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import { DataSource, DataSourceManager } from '../index';
|
|
12
12
|
import { FlowEngine } from '../../flowEngine';
|
|
13
13
|
|
|
@@ -79,4 +79,37 @@ describe('DataSource & Collection APIs', () => {
|
|
|
79
79
|
]),
|
|
80
80
|
).toThrow(/circular/);
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
it('ensureLoaded, reload and data source events work for main loader', async () => {
|
|
84
|
+
const { m, engine } = makeManager();
|
|
85
|
+
const loadedListener = vi.fn();
|
|
86
|
+
const failedListener = vi.fn();
|
|
87
|
+
engine.context.app = { eventBus: new EventTarget() } as any;
|
|
88
|
+
engine.context.app.eventBus.addEventListener('dataSource:loaded', loadedListener);
|
|
89
|
+
engine.context.app.eventBus.addEventListener('dataSource:loadFailed', failedListener);
|
|
90
|
+
|
|
91
|
+
const loader = vi
|
|
92
|
+
.fn()
|
|
93
|
+
.mockResolvedValueOnce({
|
|
94
|
+
collections: [{ name: 'posts', fields: [{ name: 'title', type: 'string', interface: 'input' }] }],
|
|
95
|
+
})
|
|
96
|
+
.mockResolvedValueOnce({
|
|
97
|
+
collections: [{ name: 'users', fields: [{ name: 'nickname', type: 'string', interface: 'input' }] }],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
m.registerLoader('main', loader);
|
|
101
|
+
|
|
102
|
+
await m.ensureLoaded();
|
|
103
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(m.getDataSource('main')?.status).toBe('loaded');
|
|
105
|
+
expect(m.getCollection('main', 'posts')?.name).toBe('posts');
|
|
106
|
+
expect(loadedListener).toHaveBeenCalledTimes(1);
|
|
107
|
+
|
|
108
|
+
await m.reloadDataSource('main');
|
|
109
|
+
expect(loader).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(m.getCollection('main', 'posts')).toBeUndefined();
|
|
111
|
+
expect(m.getCollection('main', 'users')?.name).toBe('users');
|
|
112
|
+
expect(m.getDataSource('main')?.reload).toBeTypeOf('function');
|
|
113
|
+
expect(failedListener).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
82
115
|
});
|