@nocobase/flow-engine 2.2.0-beta.17 → 2.2.0-beta.18
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/subModel/LazyDropdown.js +21 -7
- package/lib/flowContext.js +5 -0
- package/lib/resources/flowResource.js +1 -0
- package/package.json +4 -4
- package/src/__tests__/runjsFormSubmit.test.ts +45 -0
- package/src/components/subModel/LazyDropdown.tsx +28 -13
- package/src/components/subModel/__tests__/LazyDropdown.test.tsx +202 -0
- package/src/flowContext.ts +4 -0
- package/src/resources/__tests__/flowResource.test.ts +3 -0
- package/src/resources/flowResource.ts +1 -0
|
@@ -44,12 +44,6 @@ var import_css = require("@emotion/css");
|
|
|
44
44
|
var import_antd = require("antd");
|
|
45
45
|
var import_react = __toESM(require("react"));
|
|
46
46
|
var import_provider = require("../../provider");
|
|
47
|
-
const useNiceDropdownMaxHeight = /* @__PURE__ */ __name(() => {
|
|
48
|
-
return (0, import_react.useMemo)(() => {
|
|
49
|
-
const maxHeight = Math.min(window.innerHeight * 0.6, 400);
|
|
50
|
-
return maxHeight;
|
|
51
|
-
}, []);
|
|
52
|
-
}, "useNiceDropdownMaxHeight");
|
|
53
47
|
const useAsyncMenuItems = /* @__PURE__ */ __name((menuVisible, rootItems, resetKey, openKeySet, refreshKeys) => {
|
|
54
48
|
const [loadedChildren, setLoadedChildren] = (0, import_react.useState)({});
|
|
55
49
|
const [loadingKeys, setLoadingKeys] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
@@ -411,6 +405,7 @@ const KEEP_OPEN_LABEL_STYLE = {
|
|
|
411
405
|
width: "100%"
|
|
412
406
|
};
|
|
413
407
|
const DROPDOWN_PERSIST_TTL_MS = 350;
|
|
408
|
+
const DEFAULT_DROPDOWN_MAX_HEIGHT = 400;
|
|
414
409
|
const MENU_CLOSE_DELAY = 0.3;
|
|
415
410
|
const SUBMENU_MOTION_DISABLED = {
|
|
416
411
|
motionEnter: false,
|
|
@@ -420,8 +415,11 @@ const dropdownPersistRegistry = /* @__PURE__ */ new Map();
|
|
|
420
415
|
const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
|
|
421
416
|
const engine = (0, import_provider.useFlowEngine)();
|
|
422
417
|
const { getPrefixCls } = import_react.default.useContext(import_antd.ConfigProvider.ConfigContext);
|
|
418
|
+
const { token } = import_antd.theme.useToken();
|
|
423
419
|
const triggerId = import_react.default.useId();
|
|
420
|
+
const showArrow = Boolean(props.arrow);
|
|
424
421
|
const [menuVisible, setMenuVisible] = (0, import_react.useState)(false);
|
|
422
|
+
const [dropdownMaxHeight, setDropdownMaxHeight] = (0, import_react.useState)(DEFAULT_DROPDOWN_MAX_HEIGHT);
|
|
425
423
|
const [openKeys, setOpenKeys] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
426
424
|
const [rootItems, setRootItems] = (0, import_react.useState)([]);
|
|
427
425
|
const [rootLoading, setRootLoading] = (0, import_react.useState)(false);
|
|
@@ -431,7 +429,6 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
|
|
|
431
429
|
const triggerOpenClassName = `nb-lazy-dropdown-trigger-${triggerId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
432
430
|
const defaultOpenClassName = `${getPrefixCls("dropdown", props.prefixCls)}-open`;
|
|
433
431
|
const mergedOpenClassName = [props.openClassName ?? defaultOpenClassName, triggerOpenClassName].filter(Boolean).join(" ");
|
|
434
|
-
const dropdownMaxHeight = useNiceDropdownMaxHeight();
|
|
435
432
|
const t = engine.translate.bind(engine);
|
|
436
433
|
const { items: menuItems, keepDropdownOpen, persistKey, stateVersion, refreshKeys, ...dropdownMenuProps } = menu;
|
|
437
434
|
const { loadedChildren, loadingKeys, handleLoadChildren } = useAsyncMenuItems(
|
|
@@ -445,6 +442,23 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
|
|
|
445
442
|
const { searchValues, inputValues, clearSearchValue, clearAllSearchValues } = searchHandlers;
|
|
446
443
|
const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
|
|
447
444
|
useSubmenuStyles(menuVisible, dropdownMaxHeight);
|
|
445
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
446
|
+
if (!menuVisible) return;
|
|
447
|
+
const updateDropdownMaxHeight = /* @__PURE__ */ __name(() => {
|
|
448
|
+
const trigger = document.querySelector(`.${triggerOpenClassName}`);
|
|
449
|
+
if (!trigger) return;
|
|
450
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
451
|
+
const placementOffset = token.marginXXS + (showArrow ? token.sizePopupArrow / 2 : 0);
|
|
452
|
+
const reservedSpace = placementOffset + token.marginXXS;
|
|
453
|
+
const availableAbove = triggerRect.top - reservedSpace;
|
|
454
|
+
const availableBelow = window.innerHeight - triggerRect.bottom - reservedSpace;
|
|
455
|
+
const nextMaxHeight = Math.min(DEFAULT_DROPDOWN_MAX_HEIGHT, Math.max(0, availableAbove, availableBelow));
|
|
456
|
+
setDropdownMaxHeight(nextMaxHeight);
|
|
457
|
+
}, "updateDropdownMaxHeight");
|
|
458
|
+
updateDropdownMaxHeight();
|
|
459
|
+
window.addEventListener("resize", updateDropdownMaxHeight);
|
|
460
|
+
return () => window.removeEventListener("resize", updateDropdownMaxHeight);
|
|
461
|
+
}, [menuVisible, showArrow, token.marginXXS, token.sizePopupArrow, triggerOpenClassName]);
|
|
448
462
|
const closeMenu = (0, import_react.useCallback)(() => {
|
|
449
463
|
setMenuVisible(false);
|
|
450
464
|
activeSearchKeyRef.current = null;
|
package/lib/flowContext.js
CHANGED
|
@@ -3511,8 +3511,13 @@ function __mergeRunJSDocMeta(base, patch) {
|
|
|
3511
3511
|
__name(__mergeRunJSDocMeta, "__mergeRunJSDocMeta");
|
|
3512
3512
|
const _FlowRunJSContext = class _FlowRunJSContext extends FlowContext {
|
|
3513
3513
|
constructor(delegate) {
|
|
3514
|
+
var _a, _b;
|
|
3514
3515
|
super();
|
|
3515
3516
|
this.addDelegate(delegate);
|
|
3517
|
+
const submit = (_b = (_a = delegate.blockModel) == null ? void 0 : _a.submitFromRunJs) == null ? void 0 : _b.bind(delegate.blockModel);
|
|
3518
|
+
if (delegate.form && submit) {
|
|
3519
|
+
this.defineProperty("form", { value: { ...delegate.form, submit } });
|
|
3520
|
+
}
|
|
3516
3521
|
this.defineProperty("React", { value: import_react.default });
|
|
3517
3522
|
this.defineProperty("antd", { value: antd });
|
|
3518
3523
|
this.defineProperty("dayjs", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.2.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.2.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-beta.18",
|
|
12
|
+
"@nocobase/shared": "2.2.0-beta.18",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "b74c962f7d35aa12012d908d16bd74fe6ac20a3d"
|
|
41
41
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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, vi } from 'vitest';
|
|
11
|
+
import { FlowContext, FlowRunJSContext } from '../flowContext';
|
|
12
|
+
import { JSItemRunJSContext } from '../runjs-context/contexts/JSItemRunJSContext';
|
|
13
|
+
|
|
14
|
+
describe('FlowRunJSContext form submission', () => {
|
|
15
|
+
it('uses the form block RunJS submit capability without changing the native form', () => {
|
|
16
|
+
const nativeSubmit = vi.fn();
|
|
17
|
+
const getFieldsValue = vi.fn(() => ({ name: 'Alice' }));
|
|
18
|
+
const form = { submit: nativeSubmit, getFieldsValue };
|
|
19
|
+
const submitFromRunJs = vi.fn();
|
|
20
|
+
const delegate = new FlowContext();
|
|
21
|
+
delegate.defineProperty('form', { value: form });
|
|
22
|
+
delegate.defineProperty('blockModel', { value: { submitFromRunJs } });
|
|
23
|
+
|
|
24
|
+
const ctx = new JSItemRunJSContext(delegate);
|
|
25
|
+
|
|
26
|
+
expect(ctx.form.getFieldsValue()).toEqual({ name: 'Alice' });
|
|
27
|
+
ctx.form.submit();
|
|
28
|
+
expect(submitFromRunJs).toHaveBeenCalledOnce();
|
|
29
|
+
expect(nativeSubmit).not.toHaveBeenCalled();
|
|
30
|
+
expect(form.submit).toBe(nativeSubmit);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps the native submit method when the block has no RunJS submit capability', () => {
|
|
34
|
+
const nativeSubmit = vi.fn();
|
|
35
|
+
const form = { submit: nativeSubmit };
|
|
36
|
+
const delegate = new FlowContext();
|
|
37
|
+
delegate.defineProperty('form', { value: form });
|
|
38
|
+
delegate.defineProperty('blockModel', { value: {} });
|
|
39
|
+
|
|
40
|
+
const ctx = new FlowRunJSContext(delegate);
|
|
41
|
+
|
|
42
|
+
ctx.form.submit();
|
|
43
|
+
expect(nativeSubmit).toHaveBeenCalledOnce();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { css } from '@emotion/css';
|
|
11
|
-
import { ConfigProvider, Dropdown, DropdownProps, Empty, Input, InputProps, Spin } from 'antd';
|
|
12
|
-
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
+
import { ConfigProvider, Dropdown, DropdownProps, Empty, Input, InputProps, Spin, theme } from 'antd';
|
|
12
|
+
import React, { FC, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
13
13
|
import { useFlowEngine } from '../../provider';
|
|
14
14
|
|
|
15
15
|
// ==================== Types ====================
|
|
@@ -69,16 +69,6 @@ interface ExtendedMenuInfo {
|
|
|
69
69
|
|
|
70
70
|
// ==================== Custom Hooks ====================
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
* 计算合适的下拉菜单最大高度
|
|
74
|
-
*/
|
|
75
|
-
const useNiceDropdownMaxHeight = () => {
|
|
76
|
-
return useMemo(() => {
|
|
77
|
-
const maxHeight = Math.min(window.innerHeight * 0.6, 400);
|
|
78
|
-
return maxHeight;
|
|
79
|
-
}, []);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
72
|
/**
|
|
83
73
|
* 处理异步菜单项加载的逻辑
|
|
84
74
|
*/
|
|
@@ -555,6 +545,7 @@ const KEEP_OPEN_LABEL_STYLE: React.CSSProperties = {
|
|
|
555
545
|
|
|
556
546
|
// 短暂保持打开状态的注册表(用于跨父节点快速重建时的恢复)
|
|
557
547
|
const DROPDOWN_PERSIST_TTL_MS = 350;
|
|
548
|
+
const DEFAULT_DROPDOWN_MAX_HEIGHT = 400;
|
|
558
549
|
const MENU_CLOSE_DELAY = 0.3;
|
|
559
550
|
const SUBMENU_MOTION_DISABLED = {
|
|
560
551
|
motionEnter: false,
|
|
@@ -565,8 +556,11 @@ const dropdownPersistRegistry: Map<string, number> = new Map();
|
|
|
565
556
|
const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
|
|
566
557
|
const engine = useFlowEngine();
|
|
567
558
|
const { getPrefixCls } = React.useContext(ConfigProvider.ConfigContext);
|
|
559
|
+
const { token } = theme.useToken();
|
|
568
560
|
const triggerId = React.useId();
|
|
561
|
+
const showArrow = Boolean(props.arrow);
|
|
569
562
|
const [menuVisible, setMenuVisible] = useState(false);
|
|
563
|
+
const [dropdownMaxHeight, setDropdownMaxHeight] = useState(DEFAULT_DROPDOWN_MAX_HEIGHT);
|
|
570
564
|
const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
|
|
571
565
|
const [rootItems, setRootItems] = useState<Item[]>([]);
|
|
572
566
|
const [rootLoading, setRootLoading] = useState(false);
|
|
@@ -578,7 +572,6 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
578
572
|
const mergedOpenClassName = [props.openClassName ?? defaultOpenClassName, triggerOpenClassName]
|
|
579
573
|
.filter(Boolean)
|
|
580
574
|
.join(' ');
|
|
581
|
-
const dropdownMaxHeight = useNiceDropdownMaxHeight();
|
|
582
575
|
const t = engine.translate.bind(engine);
|
|
583
576
|
|
|
584
577
|
// 解构 menu,避免在 effect 中直接依赖整个对象,减少不必要的重跑并满足 exhaustive-deps
|
|
@@ -597,6 +590,28 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
597
590
|
const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
|
|
598
591
|
useSubmenuStyles(menuVisible, dropdownMaxHeight);
|
|
599
592
|
|
|
593
|
+
useLayoutEffect(() => {
|
|
594
|
+
if (!menuVisible) return;
|
|
595
|
+
|
|
596
|
+
const updateDropdownMaxHeight = () => {
|
|
597
|
+
const trigger = document.querySelector<HTMLElement>(`.${triggerOpenClassName}`);
|
|
598
|
+
if (!trigger) return;
|
|
599
|
+
|
|
600
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
601
|
+
const placementOffset = token.marginXXS + (showArrow ? token.sizePopupArrow / 2 : 0);
|
|
602
|
+
const reservedSpace = placementOffset + token.marginXXS;
|
|
603
|
+
const availableAbove = triggerRect.top - reservedSpace;
|
|
604
|
+
const availableBelow = window.innerHeight - triggerRect.bottom - reservedSpace;
|
|
605
|
+
const nextMaxHeight = Math.min(DEFAULT_DROPDOWN_MAX_HEIGHT, Math.max(0, availableAbove, availableBelow));
|
|
606
|
+
|
|
607
|
+
setDropdownMaxHeight(nextMaxHeight);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
updateDropdownMaxHeight();
|
|
611
|
+
window.addEventListener('resize', updateDropdownMaxHeight);
|
|
612
|
+
return () => window.removeEventListener('resize', updateDropdownMaxHeight);
|
|
613
|
+
}, [menuVisible, showArrow, token.marginXXS, token.sizePopupArrow, triggerOpenClassName]);
|
|
614
|
+
|
|
600
615
|
const closeMenu = useCallback(() => {
|
|
601
616
|
setMenuVisible(false);
|
|
602
617
|
activeSearchKeyRef.current = null;
|
|
@@ -0,0 +1,202 @@
|
|
|
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 { act, render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
|
11
|
+
import { ConfigProvider } from 'antd';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { FlowEngineProvider } from '../../../provider';
|
|
15
|
+
import { FlowEngine } from '../../../flowEngine';
|
|
16
|
+
import LazyDropdown from '../LazyDropdown';
|
|
17
|
+
|
|
18
|
+
const setViewportHeight = (height: number) => {
|
|
19
|
+
Object.defineProperty(window, 'innerHeight', {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: height,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('LazyDropdown', () => {
|
|
26
|
+
const originalInnerHeight = window.innerHeight;
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
setViewportHeight(originalInnerHeight);
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses the current viewport space when opening after the viewport height changes', async () => {
|
|
34
|
+
setViewportHeight(720);
|
|
35
|
+
const engine = new FlowEngine();
|
|
36
|
+
const user = userEvent.setup();
|
|
37
|
+
|
|
38
|
+
render(
|
|
39
|
+
<FlowEngineProvider engine={engine}>
|
|
40
|
+
<ConfigProvider>
|
|
41
|
+
<LazyDropdown
|
|
42
|
+
trigger={['click']}
|
|
43
|
+
menu={{
|
|
44
|
+
items: [{ key: 'field', label: 'Field' }],
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<button type="button">Open fields</button>
|
|
48
|
+
</LazyDropdown>
|
|
49
|
+
</ConfigProvider>
|
|
50
|
+
</FlowEngineProvider>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const trigger = screen.getByRole('button', { name: 'Open fields' });
|
|
54
|
+
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue({
|
|
55
|
+
bottom: 196,
|
|
56
|
+
height: 32,
|
|
57
|
+
left: 49,
|
|
58
|
+
right: 141,
|
|
59
|
+
top: 164,
|
|
60
|
+
width: 92,
|
|
61
|
+
x: 49,
|
|
62
|
+
y: 164,
|
|
63
|
+
toJSON: () => ({}),
|
|
64
|
+
});
|
|
65
|
+
setViewportHeight(460);
|
|
66
|
+
|
|
67
|
+
await user.click(trigger);
|
|
68
|
+
|
|
69
|
+
const menu = await screen.findByRole('menu');
|
|
70
|
+
await waitFor(() => expect(menu).toHaveStyle({ maxHeight: '256px', overflowY: 'auto' }));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('updates the available height while the dropdown is open', async () => {
|
|
74
|
+
setViewportHeight(720);
|
|
75
|
+
const engine = new FlowEngine();
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
|
|
78
|
+
render(
|
|
79
|
+
<FlowEngineProvider engine={engine}>
|
|
80
|
+
<ConfigProvider>
|
|
81
|
+
<LazyDropdown
|
|
82
|
+
trigger={['click']}
|
|
83
|
+
menu={{
|
|
84
|
+
items: [{ key: 'field', label: 'Field' }],
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<button type="button">Open fields</button>
|
|
88
|
+
</LazyDropdown>
|
|
89
|
+
</ConfigProvider>
|
|
90
|
+
</FlowEngineProvider>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const trigger = screen.getByRole('button', { name: 'Open fields' });
|
|
94
|
+
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue({
|
|
95
|
+
bottom: 196,
|
|
96
|
+
height: 32,
|
|
97
|
+
left: 49,
|
|
98
|
+
right: 141,
|
|
99
|
+
top: 164,
|
|
100
|
+
width: 92,
|
|
101
|
+
x: 49,
|
|
102
|
+
y: 164,
|
|
103
|
+
toJSON: () => ({}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await user.click(trigger);
|
|
107
|
+
|
|
108
|
+
const menu = await screen.findByRole('menu');
|
|
109
|
+
await waitFor(() => expect(menu).toHaveStyle({ maxHeight: '400px', overflowY: 'auto' }));
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
setViewportHeight(460);
|
|
113
|
+
window.dispatchEvent(new Event('resize'));
|
|
114
|
+
});
|
|
115
|
+
await waitFor(() => expect(menu).toHaveStyle({ maxHeight: '256px', overflowY: 'auto' }));
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
setViewportHeight(720);
|
|
119
|
+
window.dispatchEvent(new Event('resize'));
|
|
120
|
+
});
|
|
121
|
+
await waitFor(() => expect(menu).toHaveStyle({ maxHeight: '400px', overflowY: 'auto' }));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('reserves the placement offset when the dropdown has an arrow', async () => {
|
|
125
|
+
setViewportHeight(460);
|
|
126
|
+
const engine = new FlowEngine();
|
|
127
|
+
const user = userEvent.setup();
|
|
128
|
+
|
|
129
|
+
render(
|
|
130
|
+
<FlowEngineProvider engine={engine}>
|
|
131
|
+
<ConfigProvider>
|
|
132
|
+
<LazyDropdown
|
|
133
|
+
arrow
|
|
134
|
+
trigger={['click']}
|
|
135
|
+
menu={{
|
|
136
|
+
items: [{ key: 'field', label: 'Field' }],
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<button type="button">Open fields</button>
|
|
140
|
+
</LazyDropdown>
|
|
141
|
+
</ConfigProvider>
|
|
142
|
+
</FlowEngineProvider>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const trigger = screen.getByRole('button', { name: 'Open fields' });
|
|
146
|
+
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue({
|
|
147
|
+
bottom: 196,
|
|
148
|
+
height: 32,
|
|
149
|
+
left: 49,
|
|
150
|
+
right: 141,
|
|
151
|
+
top: 164,
|
|
152
|
+
width: 92,
|
|
153
|
+
x: 49,
|
|
154
|
+
y: 164,
|
|
155
|
+
toJSON: () => ({}),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await user.click(trigger);
|
|
159
|
+
|
|
160
|
+
const menu = await screen.findByRole('menu');
|
|
161
|
+
await waitFor(() => expect(menu).toHaveStyle({ maxHeight: '248px', overflowY: 'auto' }));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('uses the space above when it is larger than the space below', async () => {
|
|
165
|
+
setViewportHeight(460);
|
|
166
|
+
const engine = new FlowEngine();
|
|
167
|
+
const user = userEvent.setup();
|
|
168
|
+
|
|
169
|
+
render(
|
|
170
|
+
<FlowEngineProvider engine={engine}>
|
|
171
|
+
<ConfigProvider>
|
|
172
|
+
<LazyDropdown
|
|
173
|
+
trigger={['click']}
|
|
174
|
+
menu={{
|
|
175
|
+
items: [{ key: 'field', label: 'Field' }],
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<button type="button">Open fields</button>
|
|
179
|
+
</LazyDropdown>
|
|
180
|
+
</ConfigProvider>
|
|
181
|
+
</FlowEngineProvider>,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const trigger = screen.getByRole('button', { name: 'Open fields' });
|
|
185
|
+
vi.spyOn(trigger, 'getBoundingClientRect').mockReturnValue({
|
|
186
|
+
bottom: 332,
|
|
187
|
+
height: 32,
|
|
188
|
+
left: 49,
|
|
189
|
+
right: 141,
|
|
190
|
+
top: 300,
|
|
191
|
+
width: 92,
|
|
192
|
+
x: 49,
|
|
193
|
+
y: 300,
|
|
194
|
+
toJSON: () => ({}),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await user.click(trigger);
|
|
198
|
+
|
|
199
|
+
const menu = await screen.findByRole('menu');
|
|
200
|
+
await waitFor(() => expect(menu).toHaveStyle({ maxHeight: '292px', overflowY: 'auto' }));
|
|
201
|
+
});
|
|
202
|
+
});
|
package/src/flowContext.ts
CHANGED
|
@@ -4611,6 +4611,10 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
4611
4611
|
constructor(delegate: FlowContext) {
|
|
4612
4612
|
super();
|
|
4613
4613
|
this.addDelegate(delegate);
|
|
4614
|
+
const submit = delegate.blockModel?.submitFromRunJs?.bind(delegate.blockModel);
|
|
4615
|
+
if (delegate.form && submit) {
|
|
4616
|
+
this.defineProperty('form', { value: { ...delegate.form, submit } });
|
|
4617
|
+
}
|
|
4614
4618
|
this.defineProperty('React', { value: React });
|
|
4615
4619
|
this.defineProperty('antd', { value: antd });
|
|
4616
4620
|
this.defineProperty('dayjs', {
|
|
@@ -79,6 +79,9 @@ describe('FlowResource - error handling', () => {
|
|
|
79
79
|
expect(r.getError()).toBeNull();
|
|
80
80
|
|
|
81
81
|
const err = new ResourceError({ response: { data: { error: { message: 'boom', code: 'X' } } } });
|
|
82
|
+
expect(err.data).toEqual({ message: 'boom', code: 'X' });
|
|
83
|
+
expect(err.message).toBe('boom');
|
|
84
|
+
expect(err.code).toBe('X');
|
|
82
85
|
const ret = r.setError(err);
|
|
83
86
|
expect(ret).toBe(r);
|
|
84
87
|
expect(r.error).toBe(err);
|