@nocobase/flow-engine 2.2.0-beta.5 → 2.2.0-beta.7
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/JSRunner.d.ts +1 -0
- package/lib/JSRunner.js +110 -20
- package/lib/components/dnd/index.js +9 -2
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
- package/lib/flowContext.js +65 -30
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/utils/index.d.ts +0 -1
- package/lib/utils/index.js +0 -11
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowContext.test.ts +43 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/components/dnd/index.tsx +11 -2
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
- package/src/flowContext.ts +68 -26
- package/src/runjs-context/helpers.ts +12 -6
- package/src/utils/index.ts +0 -9
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/lib/utils/safeGlobals.d.ts +0 -28
- package/lib/utils/safeGlobals.js +0 -367
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
- package/src/utils/__tests__/safeGlobals.test.ts +0 -106
- package/src/utils/safeGlobals.ts +0 -406
package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { cleanup, fireEvent, render, waitFor, within } from '@testing-library/react';
|
|
11
|
+
import { act, cleanup, fireEvent, render, waitFor, within } from '@testing-library/react';
|
|
12
12
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { App, ConfigProvider } from 'antd';
|
|
14
14
|
import { FlowEngine } from '../../../../../flowEngine';
|
|
@@ -17,7 +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
|
+
import { DndProvider, DragHandler, TOOLBAR_DRAG_ACTIVITY_EVENT } from '../../../../dnd';
|
|
21
21
|
|
|
22
22
|
const mockColorTextTertiary = '#8c8c8c';
|
|
23
23
|
|
|
@@ -182,12 +182,24 @@ const ToolbarDragItem = ({ model }: { model: FlowModel }) => {
|
|
|
182
182
|
);
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
+
const ForkedToolbarDragItem = ({ model }: { model: FlowModel }) => {
|
|
186
|
+
return (
|
|
187
|
+
<DragHandler model={model}>
|
|
188
|
+
<button type="button" aria-label="forked-toolbar-drag">
|
|
189
|
+
drag
|
|
190
|
+
</button>
|
|
191
|
+
</DragHandler>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
185
195
|
describe('FlowsFloatContextMenu', () => {
|
|
186
196
|
const originalResizeObserver = globalThis.ResizeObserver;
|
|
187
197
|
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
188
198
|
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
|
|
199
|
+
const originalPointerEvent = globalThis.PointerEvent;
|
|
189
200
|
|
|
190
201
|
beforeEach(() => {
|
|
202
|
+
globalThis.PointerEvent = MouseEvent as typeof PointerEvent;
|
|
191
203
|
globalThis.ResizeObserver = class {
|
|
192
204
|
observe = vi.fn();
|
|
193
205
|
disconnect = vi.fn();
|
|
@@ -207,6 +219,7 @@ describe('FlowsFloatContextMenu', () => {
|
|
|
207
219
|
globalThis.ResizeObserver = originalResizeObserver;
|
|
208
220
|
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
|
|
209
221
|
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
|
|
222
|
+
globalThis.PointerEvent = originalPointerEvent;
|
|
210
223
|
document.body.innerHTML = '';
|
|
211
224
|
});
|
|
212
225
|
|
|
@@ -545,28 +558,163 @@ describe('FlowsFloatContextMenu', () => {
|
|
|
545
558
|
fireEvent.mouseEnter(icons, { relatedTarget: host });
|
|
546
559
|
|
|
547
560
|
const dragButton = within(overlay).getByLabelText('toolbar-drag');
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
561
|
+
act(() => {
|
|
562
|
+
dragButton.ownerDocument.dispatchEvent(
|
|
563
|
+
new CustomEvent(TOOLBAR_DRAG_ACTIVITY_EVENT, {
|
|
564
|
+
detail: { active: true, modelUid: model.uid },
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
});
|
|
553
568
|
fireEvent.mouseLeave(icons, { relatedTarget: document.createElement('div') });
|
|
554
569
|
|
|
555
570
|
await waitFor(() => {
|
|
556
571
|
expect(queryOverlay(appContainer, 'drag-toolbar-model')?.className).toContain('nb-toolbar-visible');
|
|
557
572
|
});
|
|
558
573
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
574
|
+
act(() => {
|
|
575
|
+
dragButton.ownerDocument.dispatchEvent(
|
|
576
|
+
new CustomEvent(TOOLBAR_DRAG_ACTIVITY_EVENT, {
|
|
577
|
+
detail: { active: false, modelUid: model.uid },
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
});
|
|
564
581
|
|
|
565
582
|
await waitFor(() => {
|
|
566
583
|
expect(queryOverlay(appContainer, 'drag-toolbar-model')).toBeNull();
|
|
567
584
|
});
|
|
568
585
|
});
|
|
569
586
|
|
|
587
|
+
it('does not let resize handle drag release close the surrounding modal wrap', async () => {
|
|
588
|
+
const engine = new FlowEngine();
|
|
589
|
+
await engine.flowSettings.forceEnable();
|
|
590
|
+
const parentModel = createModel(engine, 'resize-parent-model');
|
|
591
|
+
const model = createModel(engine, 'resize-child-model');
|
|
592
|
+
model.setParent(parentModel);
|
|
593
|
+
const appContainer = createAppContainer();
|
|
594
|
+
const modalWrap = createPopupRoot('ant-modal-wrap');
|
|
595
|
+
const onModalWrapClick = vi.fn();
|
|
596
|
+
modalWrap.addEventListener('click', onModalWrapClick);
|
|
597
|
+
appContainer.appendChild(modalWrap);
|
|
598
|
+
mockRect(appContainer, { top: 0, left: 0, width: 1280, height: 900 });
|
|
599
|
+
mockRect(modalWrap, { top: 100, left: 200, width: 640, height: 520 });
|
|
600
|
+
|
|
601
|
+
const { getByTestId } = renderWithProviders(
|
|
602
|
+
engine,
|
|
603
|
+
<FlowsFloatContextMenu model={model} showDragHandle>
|
|
604
|
+
<div data-testid="resize-content">content</div>
|
|
605
|
+
</FlowsFloatContextMenu>,
|
|
606
|
+
{ container: modalWrap },
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const host = getHost(getByTestId('resize-content'));
|
|
610
|
+
mockRect(host, { top: 140, left: 280, width: 220, height: 48 });
|
|
611
|
+
|
|
612
|
+
fireEvent.mouseEnter(host);
|
|
613
|
+
|
|
614
|
+
const overlay = await waitFor(() => {
|
|
615
|
+
const nextOverlay = modalWrap.querySelector('[data-model-uid="resize-child-model"]') as HTMLDivElement | null;
|
|
616
|
+
expect(nextOverlay).toBeTruthy();
|
|
617
|
+
return nextOverlay as HTMLDivElement;
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const resizeHandle = overlay.querySelector('.resize-handle-right') as HTMLDivElement;
|
|
621
|
+
expect(resizeHandle).toBeTruthy();
|
|
622
|
+
|
|
623
|
+
fireEvent.mouseDown(resizeHandle, { clientX: 500, clientY: 164 });
|
|
624
|
+
fireEvent.mouseMove(document, { clientX: 460, clientY: 164 });
|
|
625
|
+
fireEvent.mouseUp(modalWrap, { clientX: 460, clientY: 164 });
|
|
626
|
+
fireEvent.click(modalWrap, { clientX: 460, clientY: 164 });
|
|
627
|
+
|
|
628
|
+
expect(onModalWrapClick).not.toHaveBeenCalled();
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('emits resize end when resize handles unmount during a drag', async () => {
|
|
632
|
+
const engine = new FlowEngine();
|
|
633
|
+
await engine.flowSettings.forceEnable();
|
|
634
|
+
const parentModel = createModel(engine, 'resize-unmount-parent-model');
|
|
635
|
+
const model = createModel(engine, 'resize-unmount-child-model');
|
|
636
|
+
model.setParent(parentModel);
|
|
637
|
+
const appContainer = createAppContainer();
|
|
638
|
+
const onResizeEnd = vi.fn();
|
|
639
|
+
parentModel.emitter.on('onResizeEnd', onResizeEnd);
|
|
640
|
+
mockRect(appContainer, { top: 0, left: 0, width: 1280, height: 900 });
|
|
641
|
+
|
|
642
|
+
const { getByTestId, unmount } = renderWithProviders(
|
|
643
|
+
engine,
|
|
644
|
+
<FlowsFloatContextMenu model={model} showDragHandle>
|
|
645
|
+
<div data-testid="resize-unmount-content">content</div>
|
|
646
|
+
</FlowsFloatContextMenu>,
|
|
647
|
+
{ container: appContainer },
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const host = getHost(getByTestId('resize-unmount-content'));
|
|
651
|
+
mockRect(host, { top: 140, left: 280, width: 220, height: 48 });
|
|
652
|
+
|
|
653
|
+
fireEvent.mouseEnter(host);
|
|
654
|
+
|
|
655
|
+
const overlay = await waitFor(() => {
|
|
656
|
+
const nextOverlay = appContainer.querySelector(
|
|
657
|
+
'[data-model-uid="resize-unmount-child-model"]',
|
|
658
|
+
) as HTMLDivElement | null;
|
|
659
|
+
expect(nextOverlay).toBeTruthy();
|
|
660
|
+
return nextOverlay as HTMLDivElement;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const resizeHandle = overlay.querySelector('.resize-handle-right') as HTMLDivElement;
|
|
664
|
+
expect(resizeHandle).toBeTruthy();
|
|
665
|
+
|
|
666
|
+
fireEvent.mouseDown(resizeHandle, { clientX: 500, clientY: 164 });
|
|
667
|
+
unmount();
|
|
668
|
+
|
|
669
|
+
expect(onResizeEnd).toHaveBeenCalledTimes(1);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('keeps the resize release click suppressed after handles unmount', async () => {
|
|
673
|
+
const engine = new FlowEngine();
|
|
674
|
+
await engine.flowSettings.forceEnable();
|
|
675
|
+
const parentModel = createModel(engine, 'resize-release-unmount-parent-model');
|
|
676
|
+
const model = createModel(engine, 'resize-release-unmount-child-model');
|
|
677
|
+
model.setParent(parentModel);
|
|
678
|
+
const appContainer = createAppContainer();
|
|
679
|
+
const modalWrap = createPopupRoot('ant-modal-wrap');
|
|
680
|
+
const onModalWrapClick = vi.fn();
|
|
681
|
+
modalWrap.addEventListener('click', onModalWrapClick);
|
|
682
|
+
appContainer.appendChild(modalWrap);
|
|
683
|
+
mockRect(appContainer, { top: 0, left: 0, width: 1280, height: 900 });
|
|
684
|
+
mockRect(modalWrap, { top: 100, left: 200, width: 640, height: 520 });
|
|
685
|
+
|
|
686
|
+
const { getByTestId, unmount } = renderWithProviders(
|
|
687
|
+
engine,
|
|
688
|
+
<FlowsFloatContextMenu model={model} showDragHandle>
|
|
689
|
+
<div data-testid="resize-release-unmount-content">content</div>
|
|
690
|
+
</FlowsFloatContextMenu>,
|
|
691
|
+
{ container: modalWrap },
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const host = getHost(getByTestId('resize-release-unmount-content'));
|
|
695
|
+
mockRect(host, { top: 140, left: 280, width: 220, height: 48 });
|
|
696
|
+
|
|
697
|
+
fireEvent.mouseEnter(host);
|
|
698
|
+
|
|
699
|
+
const overlay = await waitFor(() => {
|
|
700
|
+
const nextOverlay = modalWrap.querySelector(
|
|
701
|
+
'[data-model-uid="resize-release-unmount-child-model"]',
|
|
702
|
+
) as HTMLDivElement | null;
|
|
703
|
+
expect(nextOverlay).toBeTruthy();
|
|
704
|
+
return nextOverlay as HTMLDivElement;
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const resizeHandle = overlay.querySelector('.resize-handle-right') as HTMLDivElement;
|
|
708
|
+
expect(resizeHandle).toBeTruthy();
|
|
709
|
+
|
|
710
|
+
fireEvent.mouseDown(resizeHandle, { clientX: 500, clientY: 164 });
|
|
711
|
+
fireEvent.mouseUp(modalWrap, { clientX: 460, clientY: 164 });
|
|
712
|
+
unmount();
|
|
713
|
+
fireEvent.click(modalWrap, { clientX: 460, clientY: 164 });
|
|
714
|
+
|
|
715
|
+
expect(onModalWrapClick).not.toHaveBeenCalled();
|
|
716
|
+
});
|
|
717
|
+
|
|
570
718
|
it('hides parent toolbar when hovering a nested child host', async () => {
|
|
571
719
|
const engine = new FlowEngine();
|
|
572
720
|
await engine.flowSettings.forceEnable();
|
|
@@ -714,6 +862,122 @@ describe('FlowsFloatContextMenu', () => {
|
|
|
714
862
|
});
|
|
715
863
|
});
|
|
716
864
|
|
|
865
|
+
it('restores parent toolbar when stale child activity remains but pointer is on the parent block', async () => {
|
|
866
|
+
const engine = new FlowEngine();
|
|
867
|
+
await engine.flowSettings.forceEnable();
|
|
868
|
+
const parentModel = createModel(engine, 'parent-stale-child-model');
|
|
869
|
+
const childModel = createModel(engine, 'child-stale-model');
|
|
870
|
+
const appContainer = createAppContainer();
|
|
871
|
+
mockRect(appContainer, { top: 0, left: 0, width: 1280, height: 900 });
|
|
872
|
+
|
|
873
|
+
const { getByTestId } = renderWithProviders(
|
|
874
|
+
engine,
|
|
875
|
+
<FlowsFloatContextMenu model={parentModel}>
|
|
876
|
+
<div data-testid="parent-content">
|
|
877
|
+
<div data-testid="parent-gap">gap</div>
|
|
878
|
+
<FlowsFloatContextMenu model={childModel}>
|
|
879
|
+
<div data-testid="child-content">child</div>
|
|
880
|
+
</FlowsFloatContextMenu>
|
|
881
|
+
</div>
|
|
882
|
+
</FlowsFloatContextMenu>,
|
|
883
|
+
{ container: appContainer },
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const parentHost = getHost(getByTestId('parent-content'));
|
|
887
|
+
const childHost = getHost(getByTestId('child-content'));
|
|
888
|
+
const parentGap = getByTestId('parent-gap');
|
|
889
|
+
mockRect(parentHost, { top: 10, left: 10, width: 320, height: 160 });
|
|
890
|
+
mockRect(childHost, { top: 28, left: 36, width: 120, height: 48 });
|
|
891
|
+
|
|
892
|
+
fireEvent.mouseEnter(parentHost);
|
|
893
|
+
|
|
894
|
+
const parentOverlay = await waitFor(() => {
|
|
895
|
+
const nextOverlay = queryOverlay(appContainer, 'parent-stale-child-model');
|
|
896
|
+
expect(nextOverlay).toBeTruthy();
|
|
897
|
+
return nextOverlay as HTMLDivElement;
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
await waitFor(() => {
|
|
901
|
+
expect(within(parentOverlay).getByLabelText('flows-settings')).toBeTruthy();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
act(() => {
|
|
905
|
+
childHost.dispatchEvent(
|
|
906
|
+
new CustomEvent('nb-float-menu-child-activity', {
|
|
907
|
+
bubbles: true,
|
|
908
|
+
detail: { active: true, modelUid: childModel.uid },
|
|
909
|
+
}),
|
|
910
|
+
);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
await waitFor(() => {
|
|
914
|
+
expect(queryOverlay(appContainer, 'parent-stale-child-model')).toBeNull();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
fireEvent.mouseMove(parentGap);
|
|
918
|
+
|
|
919
|
+
await waitFor(() => {
|
|
920
|
+
const parentOverlayAfterRestore = queryOverlay(appContainer, 'parent-stale-child-model');
|
|
921
|
+
expect(parentOverlayAfterRestore).toBeTruthy();
|
|
922
|
+
expect(parentOverlayAfterRestore?.className).toContain('nb-toolbar-visible');
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('keeps parent toolbar hidden when moving over the parent block while child toolbar is active', async () => {
|
|
927
|
+
const engine = new FlowEngine();
|
|
928
|
+
await engine.flowSettings.forceEnable();
|
|
929
|
+
const parentModel = createModel(engine, 'parent-active-child-model');
|
|
930
|
+
const childModel = createModel(engine, 'child-active-model');
|
|
931
|
+
const appContainer = createAppContainer();
|
|
932
|
+
mockRect(appContainer, { top: 0, left: 0, width: 1280, height: 900 });
|
|
933
|
+
|
|
934
|
+
const { getByTestId } = renderWithProviders(
|
|
935
|
+
engine,
|
|
936
|
+
<FlowsFloatContextMenu model={parentModel}>
|
|
937
|
+
<div data-testid="parent-content">
|
|
938
|
+
<div data-testid="parent-gap">gap</div>
|
|
939
|
+
<FlowsFloatContextMenu model={childModel}>
|
|
940
|
+
<div data-testid="child-content">child</div>
|
|
941
|
+
</FlowsFloatContextMenu>
|
|
942
|
+
</div>
|
|
943
|
+
</FlowsFloatContextMenu>,
|
|
944
|
+
{ container: appContainer },
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
const parentHost = getHost(getByTestId('parent-content'));
|
|
948
|
+
const childHost = getHost(getByTestId('child-content'));
|
|
949
|
+
const parentGap = getByTestId('parent-gap');
|
|
950
|
+
mockRect(parentHost, { top: 10, left: 10, width: 320, height: 160 });
|
|
951
|
+
mockRect(childHost, { top: 28, left: 36, width: 120, height: 48 });
|
|
952
|
+
|
|
953
|
+
fireEvent.mouseEnter(parentHost);
|
|
954
|
+
|
|
955
|
+
await waitFor(() => {
|
|
956
|
+
expect(queryOverlay(appContainer, 'parent-active-child-model')).toBeTruthy();
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
fireEvent.mouseEnter(childHost);
|
|
960
|
+
fireEvent.mouseMove(childHost);
|
|
961
|
+
|
|
962
|
+
const childOverlay = await waitFor(() => {
|
|
963
|
+
const nextOverlay = queryOverlay(appContainer, 'child-active-model');
|
|
964
|
+
expect(nextOverlay).toBeTruthy();
|
|
965
|
+
return nextOverlay as HTMLDivElement;
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
await waitFor(() => {
|
|
969
|
+
expect(childOverlay.className).toContain('nb-toolbar-visible');
|
|
970
|
+
expect(queryOverlay(appContainer, 'parent-active-child-model')).toBeNull();
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
fireEvent.mouseMove(parentGap);
|
|
974
|
+
|
|
975
|
+
await waitFor(() => {
|
|
976
|
+
expect(queryOverlay(appContainer, 'child-active-model')?.className).toContain('nb-toolbar-visible');
|
|
977
|
+
expect(queryOverlay(appContainer, 'parent-active-child-model')).toBeNull();
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
717
981
|
it('treats forked models as distinct float menu instances even when they share the same uid', async () => {
|
|
718
982
|
const engine = new FlowEngine();
|
|
719
983
|
await engine.flowSettings.forceEnable();
|
|
@@ -775,4 +1039,109 @@ describe('FlowsFloatContextMenu', () => {
|
|
|
775
1039
|
expect(firstOverlay.getAttribute('data-model-uid')).toBe(firstInstanceId);
|
|
776
1040
|
expect(secondOverlay.getAttribute('data-model-uid')).toBe(secondInstanceId);
|
|
777
1041
|
});
|
|
1042
|
+
|
|
1043
|
+
it('keeps a forked child toolbar active while dragging one of its toolbar items', async () => {
|
|
1044
|
+
const engine = new FlowEngine();
|
|
1045
|
+
await engine.flowSettings.forceEnable();
|
|
1046
|
+
const parentModel = createModel(engine, 'parent-forked-drag-model');
|
|
1047
|
+
const childMasterModel = new FlowModel({ uid: 'forked-drag-child-model', flowEngine: engine });
|
|
1048
|
+
childMasterModel.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
1049
|
+
const childFork = childMasterModel.createFork({}, 'drag-card') as FlowModel & { forkId?: string };
|
|
1050
|
+
const childInstanceId = `forked-drag-child-model::${String(childFork.forkId)}`;
|
|
1051
|
+
const appContainer = createAppContainer();
|
|
1052
|
+
mockRect(appContainer, { top: 0, left: 0, width: 1280, height: 900 });
|
|
1053
|
+
|
|
1054
|
+
const { getByTestId } = renderWithProviders(
|
|
1055
|
+
engine,
|
|
1056
|
+
<DndProvider>
|
|
1057
|
+
<FlowsFloatContextMenu model={parentModel}>
|
|
1058
|
+
<div data-testid="parent-content">
|
|
1059
|
+
<div data-testid="parent-gap">gap</div>
|
|
1060
|
+
<FlowsFloatContextMenu
|
|
1061
|
+
model={childFork}
|
|
1062
|
+
extraToolbarItems={[
|
|
1063
|
+
{
|
|
1064
|
+
key: 'forked-toolbar-drag',
|
|
1065
|
+
component: ForkedToolbarDragItem,
|
|
1066
|
+
sort: 100,
|
|
1067
|
+
},
|
|
1068
|
+
]}
|
|
1069
|
+
>
|
|
1070
|
+
<div data-testid="forked-child-content">child</div>
|
|
1071
|
+
</FlowsFloatContextMenu>
|
|
1072
|
+
</div>
|
|
1073
|
+
</FlowsFloatContextMenu>
|
|
1074
|
+
</DndProvider>,
|
|
1075
|
+
{ container: appContainer },
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
const parentHost = getHost(getByTestId('parent-content'));
|
|
1079
|
+
const childHost = getHost(getByTestId('forked-child-content'));
|
|
1080
|
+
const parentGap = getByTestId('parent-gap');
|
|
1081
|
+
mockRect(parentHost, { top: 10, left: 10, width: 320, height: 160 });
|
|
1082
|
+
mockRect(childHost, { top: 28, left: 36, width: 120, height: 48 });
|
|
1083
|
+
|
|
1084
|
+
fireEvent.mouseEnter(parentHost);
|
|
1085
|
+
|
|
1086
|
+
await waitFor(() => {
|
|
1087
|
+
expect(queryOverlay(appContainer, 'parent-forked-drag-model')).toBeTruthy();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
fireEvent.mouseEnter(childHost);
|
|
1091
|
+
fireEvent.mouseMove(childHost);
|
|
1092
|
+
|
|
1093
|
+
const childOverlay = await waitFor(() => {
|
|
1094
|
+
const nextOverlay = queryOverlay(appContainer, childInstanceId);
|
|
1095
|
+
expect(nextOverlay).toBeTruthy();
|
|
1096
|
+
return nextOverlay as HTMLDivElement;
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
await waitFor(() => {
|
|
1100
|
+
expect(queryOverlay(appContainer, 'parent-forked-drag-model')).toBeNull();
|
|
1101
|
+
expect(within(childOverlay).getByLabelText('forked-toolbar-drag')).toBeTruthy();
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
const childIcons = childOverlay.querySelector('.nb-toolbar-container-icons') as HTMLDivElement;
|
|
1105
|
+
fireEvent.mouseLeave(parentHost, { relatedTarget: childIcons });
|
|
1106
|
+
fireEvent.mouseLeave(childHost, { relatedTarget: childIcons });
|
|
1107
|
+
fireEvent.mouseEnter(childIcons, { relatedTarget: childHost });
|
|
1108
|
+
|
|
1109
|
+
const dragButton = within(childOverlay).getByLabelText('forked-toolbar-drag');
|
|
1110
|
+
const dragActivityListener = vi.fn();
|
|
1111
|
+
dragButton.ownerDocument.addEventListener(TOOLBAR_DRAG_ACTIVITY_EVENT, dragActivityListener);
|
|
1112
|
+
fireEvent.pointerDown(dragButton.parentElement as HTMLElement, { button: 0, clientX: 48, clientY: 36 });
|
|
1113
|
+
|
|
1114
|
+
expect(dragActivityListener).toHaveBeenCalledWith(
|
|
1115
|
+
expect.objectContaining({
|
|
1116
|
+
detail: expect.objectContaining({ active: true, modelUid: childInstanceId }),
|
|
1117
|
+
}),
|
|
1118
|
+
);
|
|
1119
|
+
dragButton.ownerDocument.removeEventListener(TOOLBAR_DRAG_ACTIVITY_EVENT, dragActivityListener);
|
|
1120
|
+
|
|
1121
|
+
vi.useFakeTimers();
|
|
1122
|
+
const querySelectorAllSpy = vi.spyOn(parentGap.ownerDocument, 'querySelectorAll');
|
|
1123
|
+
|
|
1124
|
+
try {
|
|
1125
|
+
fireEvent.mouseLeave(childIcons, { relatedTarget: parentGap });
|
|
1126
|
+
fireEvent.mouseEnter(parentHost, { relatedTarget: childIcons });
|
|
1127
|
+
fireEvent.mouseEnter(parentGap, { relatedTarget: childIcons });
|
|
1128
|
+
fireEvent.mouseMove(parentGap);
|
|
1129
|
+
|
|
1130
|
+
expect(querySelectorAllSpy).not.toHaveBeenCalled();
|
|
1131
|
+
|
|
1132
|
+
await act(async () => {
|
|
1133
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
1134
|
+
});
|
|
1135
|
+
} finally {
|
|
1136
|
+
querySelectorAllSpy.mockRestore();
|
|
1137
|
+
vi.useRealTimers();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
await waitFor(() => {
|
|
1141
|
+
const childOverlayAfterDrag = queryOverlay(appContainer, childInstanceId);
|
|
1142
|
+
expect(childOverlayAfterDrag).toBeTruthy();
|
|
1143
|
+
expect(childOverlayAfterDrag?.className).toContain('nb-toolbar-visible');
|
|
1144
|
+
expect(queryOverlay(appContainer, 'parent-forked-drag-model')).toBeNull();
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
778
1147
|
});
|
|
@@ -47,6 +47,25 @@ const getToolbarModelUidFromTarget = (target: EventTarget | null): string | null
|
|
|
47
47
|
return target.closest('.nb-toolbar-container[data-model-uid]')?.getAttribute('data-model-uid') || null;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
const escapeCssAttributeValue = (value: string): string => {
|
|
51
|
+
return value
|
|
52
|
+
.replace(/\\/g, '\\\\')
|
|
53
|
+
.replace(/"/g, '\\"')
|
|
54
|
+
.replace(/\n/g, '\\a ')
|
|
55
|
+
.replace(/\r/g, '\\d ')
|
|
56
|
+
.replace(/\f/g, '\\c ');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const isToolbarModelUidRendered = (ownerDocument: Document | null, modelUid: string): boolean => {
|
|
60
|
+
if (!ownerDocument) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return !!ownerDocument.querySelector<HTMLElement>(
|
|
65
|
+
`.nb-toolbar-container[data-model-uid="${escapeCssAttributeValue(modelUid)}"]`,
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
50
69
|
const isNodeWithinDescendantFloatToolbar = (
|
|
51
70
|
target: EventTarget | null,
|
|
52
71
|
container: HTMLElement | null,
|
|
@@ -264,6 +283,15 @@ export const useFloatToolbarVisibility = ({
|
|
|
264
283
|
|
|
265
284
|
if (isCurrentHostTarget) {
|
|
266
285
|
clearHideToolbarTimer();
|
|
286
|
+
setActiveChildToolbarIds((prevIds) => {
|
|
287
|
+
if (!prevIds.length) {
|
|
288
|
+
return prevIds;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const ownerDocument = containerRef.current?.ownerDocument ?? null;
|
|
292
|
+
const nextIds = prevIds.filter((id) => isToolbarModelUidRendered(ownerDocument, id));
|
|
293
|
+
return nextIds.length === prevIds.length ? prevIds : nextIds;
|
|
294
|
+
});
|
|
267
295
|
setHostHovered(true);
|
|
268
296
|
}
|
|
269
297
|
|
package/src/flowContext.ts
CHANGED
|
@@ -3139,6 +3139,37 @@ class BaseFlowModelContext extends BaseFlowEngineContext {
|
|
|
3139
3139
|
declare makeResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
|
|
3140
3140
|
}
|
|
3141
3141
|
|
|
3142
|
+
const OPEN_VIEW_INHERITED_INPUT_ARG_KEYS = [
|
|
3143
|
+
'dataSourceKey',
|
|
3144
|
+
'collectionName',
|
|
3145
|
+
'associationName',
|
|
3146
|
+
'filterByTk',
|
|
3147
|
+
'sourceId',
|
|
3148
|
+
'tabUid',
|
|
3149
|
+
];
|
|
3150
|
+
|
|
3151
|
+
function pickDefinedKeys(source: Record<string, unknown> | null | undefined, keys: string[]) {
|
|
3152
|
+
const res: Record<string, unknown> = {};
|
|
3153
|
+
for (const key of keys) {
|
|
3154
|
+
if (typeof source?.[key] !== 'undefined') {
|
|
3155
|
+
res[key] = source[key];
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
return res;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
function pickDefinedOpenViewInputArgs(source?: Record<string, unknown> | null) {
|
|
3162
|
+
return pickDefinedKeys(source, OPEN_VIEW_INHERITED_INPUT_ARG_KEYS);
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function applyDefinedDefaults(target: Record<string, unknown>, defaults: Record<string, unknown>) {
|
|
3166
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
3167
|
+
if (typeof target[key] === 'undefined') {
|
|
3168
|
+
target[key] = value;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3142
3173
|
export class FlowEngineContext extends BaseFlowEngineContext {
|
|
3143
3174
|
// public dataSourceManager: DataSourceManager;
|
|
3144
3175
|
constructor(public engine: FlowEngine) {
|
|
@@ -3398,7 +3429,19 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3398
3429
|
}),
|
|
3399
3430
|
});
|
|
3400
3431
|
this.defineProperty('role', {
|
|
3401
|
-
get: () =>
|
|
3432
|
+
get: () => {
|
|
3433
|
+
const currentRole = this.api?.auth?.role;
|
|
3434
|
+
if (currentRole !== '__union__') {
|
|
3435
|
+
return currentRole;
|
|
3436
|
+
}
|
|
3437
|
+
const roles = this.user?.roles;
|
|
3438
|
+
if (!Array.isArray(roles)) {
|
|
3439
|
+
return [];
|
|
3440
|
+
}
|
|
3441
|
+
return roles
|
|
3442
|
+
.map((role: { name?: string }) => role?.name)
|
|
3443
|
+
.filter((name: string | undefined): name is string => !!name);
|
|
3444
|
+
},
|
|
3402
3445
|
cache: false,
|
|
3403
3446
|
// 注意:使用惰性 meta 工厂,避免在 i18n 尚未注入时提前求值导致无法翻译
|
|
3404
3447
|
meta: Object.assign(() => ({ type: 'string', title: this.t('Current role'), sort: 990 }), {
|
|
@@ -3561,7 +3604,17 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3561
3604
|
doc = {};
|
|
3562
3605
|
}
|
|
3563
3606
|
const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
|
|
3564
|
-
const
|
|
3607
|
+
const browserGlobals: Record<string, any> = {};
|
|
3608
|
+
if (typeof window !== 'undefined') {
|
|
3609
|
+
browserGlobals.window = window;
|
|
3610
|
+
if (typeof navigator !== 'undefined') {
|
|
3611
|
+
browserGlobals.navigator = navigator;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
if (typeof document !== 'undefined') {
|
|
3615
|
+
browserGlobals.document = document;
|
|
3616
|
+
}
|
|
3617
|
+
const globals: Record<string, any> = { ctx: deprecatedCtx, ...browserGlobals, ...(options?.globals || {}) };
|
|
3565
3618
|
const { timeoutMs } = options || {};
|
|
3566
3619
|
return new JSRunner({ globals, timeoutMs });
|
|
3567
3620
|
});
|
|
@@ -3687,7 +3740,14 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3687
3740
|
},
|
|
3688
3741
|
});
|
|
3689
3742
|
this.defineMethod('openView', async function (uid: string, options) {
|
|
3690
|
-
const
|
|
3743
|
+
const inheritedInputArgs = {
|
|
3744
|
+
...(typeof this.model?.['getInputArgs'] === 'function'
|
|
3745
|
+
? pickDefinedOpenViewInputArgs(this.model['getInputArgs']())
|
|
3746
|
+
: {}),
|
|
3747
|
+
...pickDefinedOpenViewInputArgs(this.inputArgs),
|
|
3748
|
+
};
|
|
3749
|
+
const opts = { ...(options || {}) };
|
|
3750
|
+
applyDefinedDefaults(opts, inheritedInputArgs);
|
|
3691
3751
|
// NOTE: when custom context is passed, route navigation must be disabled to avoid losing it after refresh.
|
|
3692
3752
|
if (opts.defineProperties || opts.defineMethods) {
|
|
3693
3753
|
opts.navigation = false; // 强制不使用路由导航, 避免刷新页面时丢失上下文
|
|
@@ -3695,15 +3755,6 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3695
3755
|
let model: FlowModel | null = null;
|
|
3696
3756
|
model = await this.engine.loadModel({ uid });
|
|
3697
3757
|
if (!model) {
|
|
3698
|
-
const pickDefined = (src: Record<string, any>, keys: string[]) => {
|
|
3699
|
-
const res: Record<string, any> = {};
|
|
3700
|
-
for (const k of keys) {
|
|
3701
|
-
if (typeof src?.[k] !== 'undefined') {
|
|
3702
|
-
res[k] = src[k];
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
return res;
|
|
3706
|
-
};
|
|
3707
3758
|
model = this.engine.createModel({
|
|
3708
3759
|
uid, // 注意: 新建的 model 应该使用 ${parentModel.uid}-xxx 形式的 uid
|
|
3709
3760
|
use: 'PopupActionModel',
|
|
@@ -3714,7 +3765,7 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3714
3765
|
popupSettings: {
|
|
3715
3766
|
openView: {
|
|
3716
3767
|
// 仅在创建时持久化一份默认配置;运行时以本次 opts 为准,避免多个 opener 互相覆盖。
|
|
3717
|
-
...
|
|
3768
|
+
...pickDefinedKeys(opts, ['dataSourceKey', 'collectionName', 'associationName', 'mode', 'size']),
|
|
3718
3769
|
},
|
|
3719
3770
|
},
|
|
3720
3771
|
},
|
|
@@ -3734,8 +3785,6 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3734
3785
|
// 统一语义:为即将打开的外部视图定义一个 PendingView(占位视图)
|
|
3735
3786
|
const pendingType = (opts?.isMobileLayout ? 'embed' : opts?.mode || 'drawer') as any;
|
|
3736
3787
|
const pendingInputArgs = { ...opts, viewUid, navigation: opts.navigation };
|
|
3737
|
-
pendingInputArgs.filterByTk = pendingInputArgs.filterByTk || this.inputArgs?.filterByTk;
|
|
3738
|
-
pendingInputArgs.sourceId = pendingInputArgs.sourceId || this.inputArgs?.sourceId;
|
|
3739
3788
|
|
|
3740
3789
|
const pendingView = {
|
|
3741
3790
|
type: pendingType,
|
|
@@ -3754,17 +3803,10 @@ export class FlowModelContext extends BaseFlowModelContext {
|
|
|
3754
3803
|
} else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
|
|
3755
3804
|
openEventName = (on as any).eventName;
|
|
3756
3805
|
}
|
|
3757
|
-
await model.dispatchEvent(
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
// ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
|
|
3762
|
-
...opts,
|
|
3763
|
-
},
|
|
3764
|
-
{
|
|
3765
|
-
debounce: true,
|
|
3766
|
-
},
|
|
3767
|
-
);
|
|
3806
|
+
await model.dispatchEvent(openEventName, {
|
|
3807
|
+
// navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
|
|
3808
|
+
...opts,
|
|
3809
|
+
});
|
|
3768
3810
|
});
|
|
3769
3811
|
this.defineMethod('getEvents', function (this: BaseFlowModelContext) {
|
|
3770
3812
|
return this.model.getEvents();
|
|
@@ -46,12 +46,17 @@ export function createJSRunnerWithVersion(this: FlowContext, options?: JSRunnerO
|
|
|
46
46
|
doc = {};
|
|
47
47
|
}
|
|
48
48
|
const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (typeof
|
|
53
|
-
|
|
49
|
+
const browserGlobals: Record<string, any> = {};
|
|
50
|
+
if (typeof window !== 'undefined') {
|
|
51
|
+
browserGlobals.window = window;
|
|
52
|
+
if (typeof navigator !== 'undefined') {
|
|
53
|
+
browserGlobals.navigator = navigator;
|
|
54
|
+
}
|
|
54
55
|
}
|
|
56
|
+
if (typeof document !== 'undefined') {
|
|
57
|
+
browserGlobals.document = document;
|
|
58
|
+
}
|
|
59
|
+
const globals: Record<string, any> = { ctx: deprecatedCtx, ...browserGlobals, ...(options?.globals || {}) };
|
|
55
60
|
// 透传 JSRunnerOptions 其余配置(如 timeoutMs)
|
|
56
61
|
const { timeoutMs } = options || {};
|
|
57
62
|
return new JSRunner({ globals, timeoutMs });
|
|
@@ -59,7 +64,8 @@ export function createJSRunnerWithVersion(this: FlowContext, options?: JSRunnerO
|
|
|
59
64
|
|
|
60
65
|
export function getRunJSScenesForModel(modelClass: string, version: RunJSVersion = 'v1'): string[] {
|
|
61
66
|
const meta = RunJSContextRegistry.getMeta(version, modelClass);
|
|
62
|
-
|
|
67
|
+
const scenes = meta?.scenes;
|
|
68
|
+
return Array.isArray(scenes) ? [...scenes] : [];
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export function getRunJSScenesForContext(ctx: FlowContext, { version = 'v1' as RunJSVersion } = {}): string[] {
|