@nocobase/flow-engine 2.2.0-alpha.2 → 2.2.0-alpha.3
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/FlowContextSelector.js +3 -2
- package/lib/components/MobilePopup.style.js +16 -5
- package/lib/components/variables/VariableInput.js +2 -1
- package/lib/components/variables/types.d.ts +10 -0
- package/package.json +4 -4
- package/src/components/FlowContextSelector.tsx +3 -2
- package/src/components/MobilePopup.style.ts +22 -6
- package/src/components/__tests__/MobilePopup.style.test.tsx +103 -0
- package/src/components/variables/VariableInput.tsx +2 -1
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +23 -3
- package/src/components/variables/__tests__/VariableInput.test.tsx +51 -5
- package/src/components/variables/types.ts +10 -0
|
@@ -90,6 +90,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
|
|
|
90
90
|
value,
|
|
91
91
|
onChange,
|
|
92
92
|
children,
|
|
93
|
+
active,
|
|
93
94
|
metaTree,
|
|
94
95
|
showSearch = false,
|
|
95
96
|
parseValueToPath: customParseValueToPath = import_utils.parseValueToPath,
|
|
@@ -220,9 +221,9 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
|
|
|
220
221
|
(0, import_utils.preloadContextSelectorPath)(options, pathToPreload, triggerUpdate);
|
|
221
222
|
}, [options, pathToPreload, triggerUpdate]);
|
|
222
223
|
const defaultChildren = (0, import_react.useMemo)(() => {
|
|
223
|
-
const hasSelected = currentPath && currentPath.length > 0;
|
|
224
|
+
const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
|
|
224
225
|
return /* @__PURE__ */ import_react.default.createElement(import_antd.Button, { type: hasSelected ? "primary" : "default", style: defaultButtonStyle }, "x");
|
|
225
|
-
}, [currentPath]);
|
|
226
|
+
}, [active, currentPath]);
|
|
226
227
|
const handleChange = (0, import_react.useCallback)(
|
|
227
228
|
(selectedValues, selectedOptions) => {
|
|
228
229
|
const lastOption = selectedOptions == null ? void 0 : selectedOptions[selectedOptions.length - 1];
|
|
@@ -96,9 +96,17 @@ const genStyleHook = /* @__PURE__ */ __name((component, styleFn) => {
|
|
|
96
96
|
return [genCommonStyle(token, prefixCls), styleInterpolation];
|
|
97
97
|
}
|
|
98
98
|
);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const wrapSSRDeps = [theme2, token, hashId, prefixCls, iconPrefixCls, rootPrefixCls, props];
|
|
100
|
+
const wrapSSRCacheRef = (0, import_react.useRef)();
|
|
101
|
+
const currentCache = wrapSSRCacheRef.current;
|
|
102
|
+
let memoizedWrapSSR = (currentCache == null ? void 0 : currentCache.wrapSSR) || wrapSSR;
|
|
103
|
+
if (!currentCache || wrapSSRDeps.some((dep, index) => dep !== currentCache.deps[index])) {
|
|
104
|
+
wrapSSRCacheRef.current = {
|
|
105
|
+
deps: wrapSSRDeps,
|
|
106
|
+
wrapSSR
|
|
107
|
+
};
|
|
108
|
+
memoizedWrapSSR = wrapSSR;
|
|
109
|
+
}
|
|
102
110
|
return {
|
|
103
111
|
wrapSSR: memoizedWrapSSR,
|
|
104
112
|
hashId,
|
|
@@ -112,7 +120,7 @@ const useMobileActionDrawerStyle = genStyleHook("nb-mobile-action-drawer", (toke
|
|
|
112
120
|
return {
|
|
113
121
|
[componentCls]: {
|
|
114
122
|
".nb-mobile-action-drawer-header": {
|
|
115
|
-
height: "var(--nb-mobile-page-header-height)",
|
|
123
|
+
height: "var(--nb-mobile-page-header-height, 46px)",
|
|
116
124
|
display: "flex",
|
|
117
125
|
alignItems: "center",
|
|
118
126
|
justifyContent: "space-between",
|
|
@@ -139,7 +147,10 @@ const useMobileActionDrawerStyle = genStyleHook("nb-mobile-action-drawer", (toke
|
|
|
139
147
|
".nb-mobile-action-drawer-body": {
|
|
140
148
|
borderTopLeftRadius: 8,
|
|
141
149
|
borderTopRightRadius: 8,
|
|
142
|
-
maxHeight: "calc(
|
|
150
|
+
maxHeight: "calc(100vh - var(--nb-mobile-page-header-height, 46px))",
|
|
151
|
+
"@supports (height: 100dvh)": {
|
|
152
|
+
maxHeight: "calc(100dvh - var(--nb-mobile-page-header-height, 46px))"
|
|
153
|
+
},
|
|
143
154
|
overflowY: "auto",
|
|
144
155
|
overflowX: "hidden",
|
|
145
156
|
backgroundColor: token.colorBgLayout,
|
|
@@ -188,7 +188,7 @@ const VariableInputComponent = /* @__PURE__ */ __name(({
|
|
|
188
188
|
}
|
|
189
189
|
}, "restoreFromValue");
|
|
190
190
|
restoreFromValue();
|
|
191
|
-
}, [resolvedMetaTree, innerValue, resolvePathFromValue, currentMetaTreeNode]);
|
|
191
|
+
}, [resolvedMetaTree, innerValue, resolvePathFromValue, currentMetaTreeNode, value]);
|
|
192
192
|
const ValueComponent = (0, import_react.useMemo)(() => {
|
|
193
193
|
const Component = renderInputComponent == null ? void 0 : renderInputComponent(resolvedMetaTreeNode);
|
|
194
194
|
const CustomComponent = resolvedMetaTreeNode == null ? void 0 : resolvedMetaTreeNode.render;
|
|
@@ -325,6 +325,7 @@ const VariableInputComponent = /* @__PURE__ */ __name(({
|
|
|
325
325
|
{
|
|
326
326
|
metaTree: resolvedMetaTree,
|
|
327
327
|
value: innerValue,
|
|
328
|
+
active: (0, import_utils.isVariableValue)(innerValue),
|
|
328
329
|
onChange: handleVariableSelect,
|
|
329
330
|
parseValueToPath: resolvePathFromValue,
|
|
330
331
|
formatPathToValue: resolveValueFromPath,
|
|
@@ -13,6 +13,16 @@ export interface FlowContextSelectorProps extends Omit<CascaderProps<ContextSele
|
|
|
13
13
|
value?: string;
|
|
14
14
|
onChange?: (value: string, metaTreeNode?: MetaTreeNode) => void;
|
|
15
15
|
children?: CascaderProps<ContextSelectorItem>['children'];
|
|
16
|
+
/**
|
|
17
|
+
* Controls whether the default `x` trigger button is rendered as active
|
|
18
|
+
* (`type="primary"`). When omitted, the selector falls back to its parsed
|
|
19
|
+
* `value` path (`true` iff a valid variable path is currently selected).
|
|
20
|
+
*
|
|
21
|
+
* Use this when callers intentionally feed synthetic paths such as
|
|
22
|
+
* `['constant']` / `['null']` into the cascader to keep menu state aligned,
|
|
23
|
+
* but only want real variable references to show the blue active button.
|
|
24
|
+
*/
|
|
25
|
+
active?: boolean;
|
|
16
26
|
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
17
27
|
parseValueToPath?: (value: string) => string[] | undefined;
|
|
18
28
|
formatPathToValue?: (item: MetaTreeNode) => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.2.0-alpha.
|
|
3
|
+
"version": "2.2.0-alpha.3",
|
|
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-alpha.
|
|
12
|
-
"@nocobase/shared": "2.2.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-alpha.3",
|
|
12
|
+
"@nocobase/shared": "2.2.0-alpha.3",
|
|
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": "e3ec19361fd7981af9fe2a7f24ad335025cbefcd"
|
|
41
41
|
}
|
|
@@ -85,6 +85,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
85
85
|
value,
|
|
86
86
|
onChange,
|
|
87
87
|
children,
|
|
88
|
+
active,
|
|
88
89
|
metaTree,
|
|
89
90
|
showSearch = false,
|
|
90
91
|
parseValueToPath: customParseValueToPath = parseValueToPath,
|
|
@@ -266,13 +267,13 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
266
267
|
|
|
267
268
|
// 默认按钮组件
|
|
268
269
|
const defaultChildren = useMemo(() => {
|
|
269
|
-
const hasSelected = currentPath && currentPath.length > 0;
|
|
270
|
+
const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
|
|
270
271
|
return (
|
|
271
272
|
<Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle}>
|
|
272
273
|
x
|
|
273
274
|
</Button>
|
|
274
275
|
);
|
|
275
|
-
}, [currentPath]);
|
|
276
|
+
}, [active, currentPath]);
|
|
276
277
|
|
|
277
278
|
// 处理选择变化事件
|
|
278
279
|
const handleChange = useCallback(
|
|
@@ -11,7 +11,7 @@ import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
|
|
|
11
11
|
import { useStyleRegister } from '@ant-design/cssinjs';
|
|
12
12
|
import { merge } from '@formily/shared';
|
|
13
13
|
import type { ComponentTokenMap } from 'antd/es/theme/interface';
|
|
14
|
-
import {
|
|
14
|
+
import { useContext, useRef } from 'react';
|
|
15
15
|
import { ConfigProvider, theme } from 'antd';
|
|
16
16
|
|
|
17
17
|
const usePrefixCls = (
|
|
@@ -83,6 +83,10 @@ type UseComponentStyleResult = {
|
|
|
83
83
|
componentCls: string;
|
|
84
84
|
rootPrefixCls: string;
|
|
85
85
|
};
|
|
86
|
+
type WrapSSRCache = {
|
|
87
|
+
deps: unknown[];
|
|
88
|
+
wrapSSR: ReturnType<typeof useStyleRegister>;
|
|
89
|
+
};
|
|
86
90
|
|
|
87
91
|
const genStyleHook = <ComponentName extends OverrideComponent>(
|
|
88
92
|
component: ComponentName,
|
|
@@ -122,9 +126,18 @@ const genStyleHook = <ComponentName extends OverrideComponent>(
|
|
|
122
126
|
|
|
123
127
|
// useStyleRegister 有 BUG,会导致重复渲染,所以这里做了一层缓存
|
|
124
128
|
// 等 https://github.com/ant-design/cssinjs/pull/176 合并后,可以去掉这层缓存
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
const wrapSSRDeps = [theme, token, hashId, prefixCls, iconPrefixCls, rootPrefixCls, props];
|
|
130
|
+
const wrapSSRCacheRef = useRef<WrapSSRCache>();
|
|
131
|
+
const currentCache = wrapSSRCacheRef.current;
|
|
132
|
+
let memoizedWrapSSR: ReturnType<typeof useStyleRegister> = currentCache?.wrapSSR || wrapSSR;
|
|
133
|
+
|
|
134
|
+
if (!currentCache || wrapSSRDeps.some((dep, index) => dep !== currentCache.deps[index])) {
|
|
135
|
+
wrapSSRCacheRef.current = {
|
|
136
|
+
deps: wrapSSRDeps,
|
|
137
|
+
wrapSSR,
|
|
138
|
+
};
|
|
139
|
+
memoizedWrapSSR = wrapSSR;
|
|
140
|
+
}
|
|
128
141
|
|
|
129
142
|
return {
|
|
130
143
|
wrapSSR: memoizedWrapSSR,
|
|
@@ -140,7 +153,7 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
140
153
|
return {
|
|
141
154
|
[componentCls]: {
|
|
142
155
|
'.nb-mobile-action-drawer-header': {
|
|
143
|
-
height: 'var(--nb-mobile-page-header-height)',
|
|
156
|
+
height: 'var(--nb-mobile-page-header-height, 46px)',
|
|
144
157
|
display: 'flex',
|
|
145
158
|
alignItems: 'center',
|
|
146
159
|
justifyContent: 'space-between',
|
|
@@ -171,7 +184,10 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
171
184
|
'.nb-mobile-action-drawer-body': {
|
|
172
185
|
borderTopLeftRadius: 8,
|
|
173
186
|
borderTopRightRadius: 8,
|
|
174
|
-
maxHeight: 'calc(
|
|
187
|
+
maxHeight: 'calc(100vh - var(--nb-mobile-page-header-height, 46px))',
|
|
188
|
+
'@supports (height: 100dvh)': {
|
|
189
|
+
maxHeight: 'calc(100dvh - var(--nb-mobile-page-header-height, 46px))',
|
|
190
|
+
},
|
|
175
191
|
overflowY: 'auto',
|
|
176
192
|
overflowX: 'hidden',
|
|
177
193
|
backgroundColor: token.colorBgLayout,
|
|
@@ -0,0 +1,103 @@
|
|
|
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 React, { CSSProperties, ReactNode } from 'react';
|
|
11
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import { ConfigProvider } from 'antd';
|
|
13
|
+
import { vi } from 'vitest';
|
|
14
|
+
import { MobilePopup } from '../MobilePopup';
|
|
15
|
+
import { useMobileActionDrawerStyle } from '../MobilePopup.style';
|
|
16
|
+
|
|
17
|
+
vi.mock('antd-mobile', () => ({
|
|
18
|
+
Popup: ({
|
|
19
|
+
bodyClassName,
|
|
20
|
+
bodyStyle,
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
visible,
|
|
24
|
+
}: {
|
|
25
|
+
bodyClassName?: string;
|
|
26
|
+
bodyStyle?: CSSProperties;
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
visible?: boolean;
|
|
30
|
+
}) =>
|
|
31
|
+
visible ? (
|
|
32
|
+
<div className={className} data-testid="popup-root">
|
|
33
|
+
<div className={`adm-popup-body ${bodyClassName || ''}`} data-testid="popup-body" style={bodyStyle}>
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
) : null,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock('antd-mobile-icons', () => ({
|
|
41
|
+
CloseOutline: () => <span data-testid="close-outline" />,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('react-i18next', () => ({
|
|
45
|
+
useTranslation: () => ({
|
|
46
|
+
t: (key: string) => key,
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
function StyleProbe() {
|
|
51
|
+
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={`${componentCls} ${hashId}`}>
|
|
55
|
+
<div className="nb-mobile-action-drawer-header" data-testid="drawer-header" />
|
|
56
|
+
<div className="nb-mobile-action-drawer-body" data-testid="drawer-body" />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getInjectedStyles() {
|
|
62
|
+
return Array.from(document.querySelectorAll('style'))
|
|
63
|
+
.map((style) => style.textContent || '')
|
|
64
|
+
.join('\n')
|
|
65
|
+
.replace(/\s+/g, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('MobilePopup styles', () => {
|
|
69
|
+
it('mounts the action drawer class on the real popup body', async () => {
|
|
70
|
+
render(
|
|
71
|
+
<MobilePopup visible title="Edit" onClose={vi.fn()}>
|
|
72
|
+
<div style={{ height: 1200 }}>Long content</div>
|
|
73
|
+
</MobilePopup>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const popupBody = await screen.findByTestId('popup-body');
|
|
77
|
+
|
|
78
|
+
expect(screen.getByTestId('popup-root')).toHaveClass('ant-nb-mobile-action-drawer');
|
|
79
|
+
expect(popupBody).toHaveClass('adm-popup-body');
|
|
80
|
+
expect(popupBody).toHaveClass('nb-mobile-action-drawer-body');
|
|
81
|
+
expect(popupBody).toHaveStyle({ padding: '0px' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('limits the mobile action drawer body by viewport height', async () => {
|
|
85
|
+
render(
|
|
86
|
+
<ConfigProvider>
|
|
87
|
+
<StyleProbe />
|
|
88
|
+
</ConfigProvider>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(getInjectedStyles()).toContain('.nb-mobile-action-drawer-body');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const styles = getInjectedStyles();
|
|
96
|
+
|
|
97
|
+
expect(styles).toContain('height:var(--nb-mobile-page-header-height,46px)');
|
|
98
|
+
expect(styles).toContain('max-height:calc(100vh-var(--nb-mobile-page-header-height,46px))');
|
|
99
|
+
expect(styles).toContain('max-height:calc(100dvh-var(--nb-mobile-page-header-height,46px))');
|
|
100
|
+
expect(styles).toContain('overflow-y:auto');
|
|
101
|
+
expect(styles).not.toContain('max-height:calc(100%-var(--nb-mobile-page-header-height))');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -192,7 +192,7 @@ const VariableInputComponent: React.FC<VariableInputProps> = ({
|
|
|
192
192
|
};
|
|
193
193
|
|
|
194
194
|
restoreFromValue();
|
|
195
|
-
}, [resolvedMetaTree, innerValue, resolvePathFromValue, currentMetaTreeNode]);
|
|
195
|
+
}, [resolvedMetaTree, innerValue, resolvePathFromValue, currentMetaTreeNode, value]);
|
|
196
196
|
|
|
197
197
|
const ValueComponent = useMemo(() => {
|
|
198
198
|
const Component = renderInputComponent?.(resolvedMetaTreeNode);
|
|
@@ -356,6 +356,7 @@ const VariableInputComponent: React.FC<VariableInputProps> = ({
|
|
|
356
356
|
<FlowContextSelector
|
|
357
357
|
metaTree={resolvedMetaTree}
|
|
358
358
|
value={innerValue}
|
|
359
|
+
active={isVariableValue(innerValue)}
|
|
359
360
|
onChange={handleVariableSelect}
|
|
360
361
|
parseValueToPath={resolvePathFromValue}
|
|
361
362
|
formatPathToValue={resolveValueFromPath}
|
|
@@ -67,6 +67,23 @@ describe('FlowContextSelector', () => {
|
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
it('uses explicit active=false to keep the default trigger unhighlighted even when value parses to a path', async () => {
|
|
71
|
+
const flowContext = createTestFlowContext();
|
|
72
|
+
|
|
73
|
+
render(
|
|
74
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
75
|
+
<FlowContextSelector
|
|
76
|
+
metaTree={() => flowContext.getPropertyMetaTree()}
|
|
77
|
+
value="{{ ctx.user.name }}"
|
|
78
|
+
active={false}
|
|
79
|
+
/>
|
|
80
|
+
</TestFlowContextWrapper>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const button = await screen.findByRole('button');
|
|
84
|
+
expect(button.className).not.toContain('ant-btn-primary');
|
|
85
|
+
});
|
|
86
|
+
|
|
70
87
|
it('should support function metaTree loading', async () => {
|
|
71
88
|
const flowContext = createTestFlowContext();
|
|
72
89
|
const metaTreeFn = vi.fn(() => flowContext.getPropertyMetaTree());
|
|
@@ -405,10 +422,13 @@ describe('FlowContextSelector', () => {
|
|
|
405
422
|
|
|
406
423
|
const clearIcon = document.querySelector('.ant-input-clear-icon') as HTMLElement | null;
|
|
407
424
|
expect(clearIcon).toBeInTheDocument();
|
|
425
|
+
if (!clearIcon) {
|
|
426
|
+
throw new Error('Expected inline clear icon to be present');
|
|
427
|
+
}
|
|
408
428
|
|
|
409
|
-
fireEvent.mouseDown(clearIcon
|
|
410
|
-
fireEvent.mouseUp(clearIcon
|
|
411
|
-
fireEvent.click(clearIcon
|
|
429
|
+
fireEvent.mouseDown(clearIcon);
|
|
430
|
+
fireEvent.mouseUp(clearIcon);
|
|
431
|
+
fireEvent.click(clearIcon);
|
|
412
432
|
|
|
413
433
|
await waitFor(() => {
|
|
414
434
|
expect(onChange).toHaveBeenCalledWith('', undefined);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import { Input } from 'antd';
|
|
11
12
|
import React from 'react';
|
|
12
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import type { ContextSelectorItem } from '../types';
|
|
@@ -112,6 +113,50 @@ describe('VariableInput', () => {
|
|
|
112
113
|
expect(selectorButton).toBeInTheDocument();
|
|
113
114
|
});
|
|
114
115
|
|
|
116
|
+
it('should not highlight the selector button for synthetic constant/null paths', async () => {
|
|
117
|
+
const flowContext = createTestFlowContext();
|
|
118
|
+
|
|
119
|
+
render(
|
|
120
|
+
<TestFlowContextWrapper context={flowContext}>
|
|
121
|
+
<VariableInput
|
|
122
|
+
value=""
|
|
123
|
+
metaTree={[
|
|
124
|
+
{ name: 'constant', title: 'Constant', type: 'string', paths: ['constant'] },
|
|
125
|
+
{ name: 'null', title: 'Null', type: 'object', paths: ['null'] },
|
|
126
|
+
...flowContext.getPropertyMetaTree(),
|
|
127
|
+
]}
|
|
128
|
+
converters={{
|
|
129
|
+
renderInputComponent: (meta) => {
|
|
130
|
+
const first = meta?.paths?.[0];
|
|
131
|
+
if (first === 'constant') {
|
|
132
|
+
return (props: any) => <input aria-label="constant-value" {...props} />;
|
|
133
|
+
}
|
|
134
|
+
if (first === 'null') {
|
|
135
|
+
return () => <Input placeholder="<Null>" readOnly />;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
},
|
|
139
|
+
resolveValueFromPath: (meta) => {
|
|
140
|
+
const first = meta?.paths?.[0];
|
|
141
|
+
if (first === 'constant') return '';
|
|
142
|
+
if (first === 'null') return null;
|
|
143
|
+
return undefined;
|
|
144
|
+
},
|
|
145
|
+
resolvePathFromValue: (currentValue) => {
|
|
146
|
+
if (currentValue === null) return ['null'];
|
|
147
|
+
const trimmed = typeof currentValue === 'string' ? currentValue.trim() : currentValue;
|
|
148
|
+
if (trimmed === '') return ['constant'];
|
|
149
|
+
return undefined;
|
|
150
|
+
},
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</TestFlowContextWrapper>,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const selectorButton = await screen.findByRole('button');
|
|
157
|
+
expect(selectorButton.className).not.toContain('ant-btn-primary');
|
|
158
|
+
});
|
|
159
|
+
|
|
115
160
|
it('should handle onChange from Input', async () => {
|
|
116
161
|
const onChange = vi.fn();
|
|
117
162
|
const flowContext = createTestFlowContext();
|
|
@@ -195,20 +240,21 @@ describe('VariableInput', () => {
|
|
|
195
240
|
|
|
196
241
|
const selectElement = container.querySelector('.ant-select');
|
|
197
242
|
expect(selectElement).toBeInTheDocument();
|
|
243
|
+
if (!selectElement) {
|
|
244
|
+
throw new Error('Expected variable tag select wrapper to be present');
|
|
245
|
+
}
|
|
198
246
|
|
|
199
247
|
// 触发鼠标悬停以显示清除按钮
|
|
200
|
-
fireEvent.mouseEnter(selectElement
|
|
248
|
+
fireEvent.mouseEnter(selectElement);
|
|
201
249
|
|
|
202
250
|
// 尝试触发清除功能
|
|
203
251
|
// 方法1: 直接触发 Select 组件的 onClear 事件
|
|
204
|
-
const selectInstance = selectElement as any;
|
|
205
|
-
|
|
206
252
|
// 尝试通过键盘事件触发清除
|
|
207
|
-
fireEvent.keyDown(selectElement
|
|
253
|
+
fireEvent.keyDown(selectElement, { key: 'Backspace', code: 'Backspace' });
|
|
208
254
|
|
|
209
255
|
// 或者尝试触发自定义的清除逻辑
|
|
210
256
|
const clearEvents = new CustomEvent('clear');
|
|
211
|
-
selectElement
|
|
257
|
+
selectElement.dispatchEvent(clearEvents);
|
|
212
258
|
|
|
213
259
|
// 检查是否调用了清除功能(可能需要调整期望)
|
|
214
260
|
// 如果清除按钮不能直接测试,我们验证组件支持清除功能
|
|
@@ -16,6 +16,16 @@ export interface FlowContextSelectorProps
|
|
|
16
16
|
value?: string;
|
|
17
17
|
onChange?: (value: string, metaTreeNode?: MetaTreeNode) => void;
|
|
18
18
|
children?: CascaderProps<ContextSelectorItem>['children'];
|
|
19
|
+
/**
|
|
20
|
+
* Controls whether the default `x` trigger button is rendered as active
|
|
21
|
+
* (`type="primary"`). When omitted, the selector falls back to its parsed
|
|
22
|
+
* `value` path (`true` iff a valid variable path is currently selected).
|
|
23
|
+
*
|
|
24
|
+
* Use this when callers intentionally feed synthetic paths such as
|
|
25
|
+
* `['constant']` / `['null']` into the cascader to keep menu state aligned,
|
|
26
|
+
* but only want real variable references to show the blue active button.
|
|
27
|
+
*/
|
|
28
|
+
active?: boolean;
|
|
19
29
|
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
20
30
|
parseValueToPath?: (value: string) => string[] | undefined;
|
|
21
31
|
formatPathToValue?: (item: MetaTreeNode) => string;
|