@nocobase/flow-engine 2.2.0-beta.13 → 2.2.0-beta.15
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 +55 -12
- package/lib/components/FormItem.js +11 -7
- package/lib/components/variables/VariableHybridInput.d.ts +9 -0
- package/lib/components/variables/VariableHybridInput.js +146 -17
- package/lib/components/variables/VariableInput.js +19 -7
- package/lib/components/variables/VariableTag.js +48 -36
- package/lib/components/variables/types.d.ts +21 -0
- package/lib/flowI18n.js +3 -3
- package/lib/types.d.ts +3 -1
- package/lib/types.js +1 -0
- package/package.json +4 -4
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/components/FlowContextSelector.tsx +66 -11
- package/src/components/FormItem.tsx +12 -7
- package/src/components/__tests__/FormItem.test.tsx +17 -2
- package/src/components/variables/VariableHybridInput.tsx +185 -14
- package/src/components/variables/VariableInput.tsx +32 -7
- package/src/components/variables/VariableTag.tsx +51 -37
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +60 -3
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +212 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +202 -6
- package/src/components/variables/__tests__/VariableTag.test.tsx +80 -0
- package/src/components/variables/types.ts +21 -0
- package/src/flowI18n.ts +8 -3
- package/src/types.ts +2 -0
|
@@ -47,35 +47,37 @@ var import_utils = require("./utils");
|
|
|
47
47
|
var import_useResolvedMetaTree = require("./useResolvedMetaTree");
|
|
48
48
|
var import_ahooks = require("ahooks");
|
|
49
49
|
var import_FlowContextProvider = require("../../FlowContextProvider");
|
|
50
|
+
const VARIABLE_PARSING_FAILED_TEXT = "Variable parsing failed";
|
|
50
51
|
const VariableTagComponent = /* @__PURE__ */ __name(({
|
|
51
52
|
value,
|
|
52
53
|
onClear,
|
|
54
|
+
disabled = false,
|
|
55
|
+
allowCustomTagInput = true,
|
|
53
56
|
className,
|
|
54
57
|
style,
|
|
55
58
|
metaTreeNode,
|
|
56
|
-
metaTree
|
|
59
|
+
metaTree,
|
|
60
|
+
resolvedPath
|
|
57
61
|
}) => {
|
|
58
62
|
const { resolvedMetaTree } = (0, import_useResolvedMetaTree.useResolvedMetaTree)(metaTree);
|
|
59
63
|
const ctx = (0, import_FlowContextProvider.useFlowContext)();
|
|
60
|
-
const { data:
|
|
64
|
+
const { data: displayState } = (0, import_ahooks.useRequest)(
|
|
61
65
|
async () => {
|
|
62
66
|
const resolveLabelFromPath = /* @__PURE__ */ __name(async (rawPath2) => {
|
|
63
|
-
if (!rawPath2) return null;
|
|
64
|
-
if (!Array.isArray(rawPath2)) return null;
|
|
65
|
-
if (!Array.isArray(resolvedMetaTree)) return null;
|
|
67
|
+
if (!rawPath2) return { label: null, resolved: false, attempted: false };
|
|
68
|
+
if (!Array.isArray(rawPath2)) return { label: null, resolved: false, attempted: false };
|
|
69
|
+
if (!Array.isArray(resolvedMetaTree)) return { label: null, resolved: false, attempted: false };
|
|
66
70
|
const topNames = new Set((resolvedMetaTree || []).map((n) => String(n == null ? void 0 : n.name)));
|
|
67
71
|
const path = !topNames.has(String(rawPath2[0])) ? rawPath2.slice(1) : rawPath2;
|
|
68
|
-
if (!path.length) return "";
|
|
72
|
+
if (!path.length) return { label: "", resolved: true, attempted: true };
|
|
69
73
|
let nodes = resolvedMetaTree;
|
|
70
74
|
const titleChain = [];
|
|
71
|
-
let matchedCount = 0;
|
|
72
75
|
for (let i = 0; i < path.length; i++) {
|
|
73
|
-
if (!nodes)
|
|
76
|
+
if (!nodes) return { label: null, resolved: false, attempted: true };
|
|
74
77
|
const seg = String(path[i]);
|
|
75
78
|
const node = nodes.find((n) => String(n == null ? void 0 : n.name) === seg);
|
|
76
|
-
if (!node)
|
|
79
|
+
if (!node) return { label: null, resolved: false, attempted: true };
|
|
77
80
|
titleChain.push(String(node.title ?? node.name ?? seg));
|
|
78
|
-
matchedCount = i + 1;
|
|
79
81
|
if (i < path.length - 1) {
|
|
80
82
|
if (Array.isArray(node.children)) {
|
|
81
83
|
nodes = node.children;
|
|
@@ -92,29 +94,37 @@ const VariableTagComponent = /* @__PURE__ */ __name(({
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
|
-
|
|
96
|
-
let label2 = titleChain.map(ctx.t).join("/");
|
|
97
|
-
if (matchedCount < path.length) {
|
|
98
|
-
const tail = path.slice(matchedCount).join("/");
|
|
99
|
-
label2 = tail ? `${label2}/${tail}` : label2;
|
|
100
|
-
}
|
|
101
|
-
return label2;
|
|
97
|
+
return { label: titleChain.map(ctx.t).join("/"), resolved: true, attempted: true };
|
|
102
98
|
}, "resolveLabelFromPath");
|
|
103
99
|
if (metaTreeNode == null ? void 0 : metaTreeNode.parentTitles) {
|
|
104
|
-
return
|
|
100
|
+
return {
|
|
101
|
+
text: [...metaTreeNode.parentTitles, metaTreeNode.title].map(ctx.t).join("/"),
|
|
102
|
+
invalid: false
|
|
103
|
+
};
|
|
105
104
|
}
|
|
106
105
|
if (metaTreeNode) {
|
|
107
|
-
const rawPath2 = (0, import_utils.parseValueToPath)(value) || metaTreeNode.paths;
|
|
108
|
-
const
|
|
109
|
-
|
|
106
|
+
const rawPath2 = resolvedPath || (0, import_utils.parseValueToPath)(value) || metaTreeNode.paths;
|
|
107
|
+
const result2 = await resolveLabelFromPath(rawPath2);
|
|
108
|
+
if (result2.resolved && result2.label != null) {
|
|
109
|
+
return { text: result2.label, invalid: false };
|
|
110
|
+
}
|
|
111
|
+
if (result2.attempted) {
|
|
112
|
+
return { text: VARIABLE_PARSING_FAILED_TEXT, invalid: true, rawValue: String(value ?? "") };
|
|
113
|
+
}
|
|
114
|
+
return { text: ctx.t(metaTreeNode.title) ?? "", invalid: false };
|
|
115
|
+
}
|
|
116
|
+
if (!value) return { text: String(value), invalid: false };
|
|
117
|
+
const rawPath = resolvedPath || (0, import_utils.parseValueToPath)(value);
|
|
118
|
+
const result = await resolveLabelFromPath(rawPath);
|
|
119
|
+
if (result.resolved && result.label != null) {
|
|
120
|
+
return { text: result.label, invalid: false };
|
|
121
|
+
}
|
|
122
|
+
if (result.attempted) {
|
|
123
|
+
return { text: VARIABLE_PARSING_FAILED_TEXT, invalid: true, rawValue: String(value ?? "") };
|
|
110
124
|
}
|
|
111
|
-
|
|
112
|
-
const rawPath = (0, import_utils.parseValueToPath)(value);
|
|
113
|
-
const label = await resolveLabelFromPath(rawPath);
|
|
114
|
-
if (label != null) return label;
|
|
115
|
-
return Array.isArray(rawPath) ? rawPath.join("/") : String(value);
|
|
125
|
+
return { text: Array.isArray(rawPath) ? rawPath.join("/") : String(value), invalid: false };
|
|
116
126
|
},
|
|
117
|
-
{ refreshDeps: [resolvedMetaTree, value, metaTreeNode] }
|
|
127
|
+
{ refreshDeps: [resolvedMetaTree, resolvedPath, value, metaTreeNode] }
|
|
118
128
|
);
|
|
119
129
|
const { token } = import_antd.theme.useToken();
|
|
120
130
|
const shrinkableOverflowItem = import_css.css`
|
|
@@ -129,12 +139,12 @@ const VariableTagComponent = /* @__PURE__ */ __name(({
|
|
|
129
139
|
}
|
|
130
140
|
`;
|
|
131
141
|
const customTagRender = /* @__PURE__ */ __name((props) => {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
return /* @__PURE__ */ import_react.default.createElement(import_antd.Tooltip, { title:
|
|
142
|
+
const fullText = (displayState == null ? void 0 : displayState.text) || (typeof props.label === "string" ? props.label : String(props.label));
|
|
143
|
+
const tooltipText = (displayState == null ? void 0 : displayState.invalid) ? displayState.rawValue || fullText : fullText;
|
|
144
|
+
return /* @__PURE__ */ import_react.default.createElement(import_antd.Tooltip, { title: tooltipText, placement: "top", getPopupContainer: () => document.body }, /* @__PURE__ */ import_react.default.createElement(
|
|
135
145
|
import_antd.Tag,
|
|
136
146
|
{
|
|
137
|
-
color: "blue",
|
|
147
|
+
color: (displayState == null ? void 0 : displayState.invalid) ? "error" : "blue",
|
|
138
148
|
style: {
|
|
139
149
|
margin: `0 ${token.marginXXS || token.marginXS}px`,
|
|
140
150
|
borderRadius: token.borderRadiusSM,
|
|
@@ -175,12 +185,14 @@ const VariableTagComponent = /* @__PURE__ */ __name(({
|
|
|
175
185
|
flex: "1 1 auto",
|
|
176
186
|
...style
|
|
177
187
|
},
|
|
178
|
-
value:
|
|
179
|
-
mode: "tags",
|
|
188
|
+
value: (displayState == null ? void 0 : displayState.text) ? [displayState.text] : [],
|
|
189
|
+
mode: allowCustomTagInput ? "tags" : "multiple",
|
|
180
190
|
open: false,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
showSearch: allowCustomTagInput,
|
|
192
|
+
searchValue: allowCustomTagInput ? void 0 : "",
|
|
193
|
+
allowClear: !disabled && !!onClear,
|
|
194
|
+
onClear: disabled ? void 0 : onClear,
|
|
195
|
+
disabled: disabled || !onClear,
|
|
184
196
|
variant: "outlined",
|
|
185
197
|
suffixIcon: null,
|
|
186
198
|
tagRender: customTagRender,
|
|
@@ -13,12 +13,30 @@ 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;
|
|
19
29
|
open?: boolean;
|
|
20
30
|
onlyLeafSelectable?: boolean;
|
|
21
31
|
ignoreFieldNames?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Footer rendered at the bottom of the dropdown. Defaults to a muted
|
|
34
|
+
* "Double click to choose entire object" hint when non-leaf selection is
|
|
35
|
+
* allowed (`onlyLeafSelectable` is false) — since double-clicking a non-leaf
|
|
36
|
+
* node selects the whole object. Pass an explicit node to override, or `null`
|
|
37
|
+
* to hide it.
|
|
38
|
+
*/
|
|
39
|
+
dropdownFooter?: React.ReactNode;
|
|
22
40
|
}
|
|
23
41
|
export interface ContextSelectorItem {
|
|
24
42
|
label: React.ReactNode;
|
|
@@ -68,7 +86,10 @@ export interface VariableInputProps {
|
|
|
68
86
|
}
|
|
69
87
|
export interface VariableTagProps {
|
|
70
88
|
value?: string;
|
|
89
|
+
resolvedPath?: Array<string | number>;
|
|
71
90
|
onClear?: () => void;
|
|
91
|
+
disabled?: boolean;
|
|
92
|
+
allowCustomTagInput?: boolean;
|
|
72
93
|
className?: string;
|
|
73
94
|
style?: React.CSSProperties;
|
|
74
95
|
metaTreeNode?: MetaTreeNode | null;
|
package/lib/flowI18n.js
CHANGED
|
@@ -81,7 +81,7 @@ const _FlowI18n = class _FlowI18n {
|
|
|
81
81
|
* @private
|
|
82
82
|
*/
|
|
83
83
|
isTemplate(str) {
|
|
84
|
-
return /\{\{\s*t\s*\(\s*["'`]
|
|
84
|
+
return /\{\{\s*t\s*\(\s*(["'`])(?:\\.|(?!\1).)*?\1\s*(?:,\s*.*?)?\s*\)\s*\}\}/.test(str);
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
87
|
* 编译模板字符串
|
|
@@ -89,8 +89,8 @@ const _FlowI18n = class _FlowI18n {
|
|
|
89
89
|
*/
|
|
90
90
|
compileTemplate(template) {
|
|
91
91
|
return template.replace(
|
|
92
|
-
/\{\{\s*t\s*\(\s*["'`](
|
|
93
|
-
(match, key, optionsStr) => {
|
|
92
|
+
/\{\{\s*t\s*\(\s*(["'`])((?:\\.|(?!\1).)*?)\1\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
|
|
93
|
+
(match, _quote, key, optionsStr) => {
|
|
94
94
|
try {
|
|
95
95
|
let templateOptions = {};
|
|
96
96
|
if (optionsStr) {
|
package/lib/types.d.ts
CHANGED
|
@@ -122,7 +122,9 @@ export declare enum ActionScene {
|
|
|
122
122
|
/** 动态事件流可用 */
|
|
123
123
|
DYNAMIC_EVENT_FLOW = 6,
|
|
124
124
|
/** 菜单项联动规则可用 */
|
|
125
|
-
MENU_LINKAGE_RULES = 7
|
|
125
|
+
MENU_LINKAGE_RULES = 7,
|
|
126
|
+
/** 标签页联动规则可用 */
|
|
127
|
+
TAB_LINKAGE_RULES = 8
|
|
126
128
|
}
|
|
127
129
|
/**
|
|
128
130
|
* Defines a reusable action with generic model type support.
|
package/lib/types.js
CHANGED
|
@@ -37,6 +37,7 @@ var ActionScene = /* @__PURE__ */ ((ActionScene2) => {
|
|
|
37
37
|
ActionScene2[ActionScene2["ACTION_LINKAGE_RULES"] = 5] = "ACTION_LINKAGE_RULES";
|
|
38
38
|
ActionScene2[ActionScene2["DYNAMIC_EVENT_FLOW"] = 6] = "DYNAMIC_EVENT_FLOW";
|
|
39
39
|
ActionScene2[ActionScene2["MENU_LINKAGE_RULES"] = 7] = "MENU_LINKAGE_RULES";
|
|
40
|
+
ActionScene2[ActionScene2["TAB_LINKAGE_RULES"] = 8] = "TAB_LINKAGE_RULES";
|
|
40
41
|
return ActionScene2;
|
|
41
42
|
})(ActionScene || {});
|
|
42
43
|
// Annotate the CommonJS export names for ESM import in node:
|
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.15",
|
|
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.15",
|
|
12
|
+
"@nocobase/shared": "2.2.0-beta.15",
|
|
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": "4c471f6b7d26adc412b2cc1d91264572296ca873"
|
|
41
41
|
}
|
|
@@ -17,6 +17,17 @@ describe('FlowI18n', () => {
|
|
|
17
17
|
expect(i18n.translate("{{ t('Hello') }}")).toBe('你好');
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
it('keeps embedded quotes of a different type inside the key', () => {
|
|
21
|
+
// A single-quoted key whose text contains double quotes (and vice versa) must not be truncated at the first inner
|
|
22
|
+
// quote.
|
|
23
|
+
const key = 'Unlike "Post-action event", it listens for data changes.';
|
|
24
|
+
const table: Record<string, string> = { [key]: '与“操作后事件”不同,它监听数据变动。' };
|
|
25
|
+
const i18n = new FlowI18n({ i18n: { t: (k: string) => table[k] ?? k } });
|
|
26
|
+
|
|
27
|
+
expect(i18n.translate(`{{t('${key}', { ns: "workflow" })}}`)).toBe(table[key]);
|
|
28
|
+
expect(i18n.translate(`{{t("It's here", { ns: "workflow" })}}`)).toBe("It's here");
|
|
29
|
+
});
|
|
30
|
+
|
|
20
31
|
it('template compile ignores malformed options', () => {
|
|
21
32
|
const spy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
22
33
|
const i18n = new FlowI18n({ i18n: { t: (k: string) => k } });
|
|
@@ -40,6 +40,18 @@ type SelectedPathInfo = {
|
|
|
40
40
|
meta?: ContextSelectorItem['meta'];
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
type MetaNodeTooltipOptions = { tooltip?: React.ReactNode };
|
|
44
|
+
|
|
45
|
+
function getMetaNodeTooltip(meta?: ContextSelectorItem['meta']): React.ReactNode {
|
|
46
|
+
if (!meta) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const metaWithTooltip = meta as ContextSelectorItem['meta'] & MetaNodeTooltipOptions;
|
|
51
|
+
const options = meta.options as MetaNodeTooltipOptions | undefined;
|
|
52
|
+
return metaWithTooltip.tooltip ?? options?.tooltip;
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
const normalizePath = (path: unknown): string[] | undefined => {
|
|
44
56
|
if (!Array.isArray(path)) {
|
|
45
57
|
return undefined;
|
|
@@ -85,6 +97,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
85
97
|
value,
|
|
86
98
|
onChange,
|
|
87
99
|
children,
|
|
100
|
+
active,
|
|
88
101
|
metaTree,
|
|
89
102
|
showSearch = false,
|
|
90
103
|
parseValueToPath: customParseValueToPath = parseValueToPath,
|
|
@@ -92,6 +105,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
92
105
|
open,
|
|
93
106
|
onlyLeafSelectable = false,
|
|
94
107
|
ignoreFieldNames,
|
|
108
|
+
dropdownFooter,
|
|
95
109
|
...cascaderProps
|
|
96
110
|
}) => {
|
|
97
111
|
const { token } = theme.useToken();
|
|
@@ -119,17 +133,28 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
119
133
|
|
|
120
134
|
// 文本国际化:仅当 label 为字符串时进行翻译
|
|
121
135
|
const baseLabel = typeof o.label === 'string' ? flowCtx.t(o.label) : o.label;
|
|
136
|
+
const labelText = typeof baseLabel === 'string' ? baseLabel : String(o.value);
|
|
137
|
+
const tooltip = getMetaNodeTooltip(meta);
|
|
138
|
+
const tooltipTitle = disabled
|
|
139
|
+
? disabledReason || tooltip || flowCtx.t('This variable is not available')
|
|
140
|
+
: tooltip;
|
|
122
141
|
|
|
123
|
-
const label =
|
|
142
|
+
const label = tooltipTitle ? (
|
|
124
143
|
<span>
|
|
125
144
|
{baseLabel}
|
|
126
145
|
<Tooltip
|
|
127
|
-
title={
|
|
128
|
-
placement="
|
|
129
|
-
|
|
146
|
+
title={typeof tooltipTitle === 'string' ? flowCtx.t(tooltipTitle) : tooltipTitle}
|
|
147
|
+
placement="top"
|
|
148
|
+
classNames={{ root: 'flow-variable-tip' }}
|
|
130
149
|
destroyTooltipOnHide
|
|
131
150
|
>
|
|
132
|
-
<QuestionCircleOutlined
|
|
151
|
+
<QuestionCircleOutlined
|
|
152
|
+
aria-label={`${labelText} tooltip`}
|
|
153
|
+
style={{
|
|
154
|
+
marginLeft: token.marginXXS,
|
|
155
|
+
color: disabled ? token.colorTextDisabled : token.colorTextDescription,
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
133
158
|
</Tooltip>
|
|
134
159
|
</span>
|
|
135
160
|
) : (
|
|
@@ -144,7 +169,7 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
144
169
|
};
|
|
145
170
|
});
|
|
146
171
|
},
|
|
147
|
-
[flowCtx],
|
|
172
|
+
[flowCtx, token.colorTextDescription, token.colorTextDisabled, token.marginXXS],
|
|
148
173
|
);
|
|
149
174
|
|
|
150
175
|
// 用于强制重新渲染的状态
|
|
@@ -265,13 +290,13 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
265
290
|
|
|
266
291
|
// 默认按钮组件
|
|
267
292
|
const defaultChildren = useMemo(() => {
|
|
268
|
-
const hasSelected = currentPath && currentPath.length > 0;
|
|
293
|
+
const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
|
|
269
294
|
return (
|
|
270
|
-
<Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle}>
|
|
295
|
+
<Button type={hasSelected ? 'primary' : 'default'} style={defaultButtonStyle} disabled={cascaderProps.disabled}>
|
|
271
296
|
x
|
|
272
297
|
</Button>
|
|
273
298
|
);
|
|
274
|
-
}, [currentPath]);
|
|
299
|
+
}, [active, cascaderProps.disabled, currentPath]);
|
|
275
300
|
|
|
276
301
|
// 处理选择变化事件
|
|
277
302
|
const handleChange = useCallback(
|
|
@@ -360,12 +385,41 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
360
385
|
[cascaderOnDropdownVisibleChange, open],
|
|
361
386
|
);
|
|
362
387
|
|
|
388
|
+
// Footer hint at the bottom of the dropdown. Defaults to the "double click to choose entire object" hint whenever
|
|
389
|
+
// non-leaf selection is allowed (double-clicking a non-leaf selects the whole object). Callers can override with
|
|
390
|
+
// their own node, or pass `null` to hide it.
|
|
391
|
+
const footerNode = useMemo(() => {
|
|
392
|
+
if (dropdownFooter !== undefined) {
|
|
393
|
+
return dropdownFooter;
|
|
394
|
+
}
|
|
395
|
+
if (onlyLeafSelectable) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
return (
|
|
399
|
+
<div
|
|
400
|
+
className={css`
|
|
401
|
+
padding: 6px 12px;
|
|
402
|
+
color: ${token.colorTextDescription};
|
|
403
|
+
border-top: 1px solid ${token.colorSplit};
|
|
404
|
+
font-size: ${token.fontSizeSM}px;
|
|
405
|
+
`}
|
|
406
|
+
>
|
|
407
|
+
{flowCtx.t('Double click to choose entire object')}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
}, [dropdownFooter, onlyLeafSelectable, token, flowCtx]);
|
|
411
|
+
|
|
363
412
|
const renderDropdown = useCallback(
|
|
364
413
|
(menu: React.ReactElement) => {
|
|
365
414
|
const cascaderMenuNode = cascaderDropdownRender ? cascaderDropdownRender(menu) : menu;
|
|
366
415
|
const cascaderMenu = React.isValidElement(cascaderMenuNode) ? cascaderMenuNode : <>{cascaderMenuNode}</>;
|
|
367
416
|
if (!isSearchEnabled || children === null) {
|
|
368
|
-
return
|
|
417
|
+
return (
|
|
418
|
+
<>
|
|
419
|
+
{cascaderMenu}
|
|
420
|
+
{footerNode}
|
|
421
|
+
</>
|
|
422
|
+
);
|
|
369
423
|
}
|
|
370
424
|
|
|
371
425
|
return (
|
|
@@ -381,10 +435,11 @@ const FlowContextSelectorComponent: React.FC<FlowContextSelectorProps> = ({
|
|
|
381
435
|
/>
|
|
382
436
|
</div>
|
|
383
437
|
{cascaderMenu}
|
|
438
|
+
{footerNode}
|
|
384
439
|
</>
|
|
385
440
|
);
|
|
386
441
|
},
|
|
387
|
-
[cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText],
|
|
442
|
+
[cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText, footerNode],
|
|
388
443
|
);
|
|
389
444
|
|
|
390
445
|
const inlinePlaceholder =
|
|
@@ -54,15 +54,20 @@ const formItemPropKeys: (keyof ExtendedFormItemProps)[] = [
|
|
|
54
54
|
'showLabel',
|
|
55
55
|
];
|
|
56
56
|
|
|
57
|
+
const modelInternalPropKeys = ['globalSort'];
|
|
58
|
+
|
|
57
59
|
export const FormItem = ({
|
|
58
60
|
children,
|
|
59
61
|
showLabel = true,
|
|
60
62
|
labelWidth,
|
|
61
63
|
...rest
|
|
62
64
|
}: ExtendedFormItemProps & ChildExtraProps) => {
|
|
65
|
+
const forwardedRest = Object.fromEntries(
|
|
66
|
+
Object.entries(rest).filter(([key]) => !modelInternalPropKeys.includes(key)),
|
|
67
|
+
) as ExtendedFormItemProps & ChildExtraProps;
|
|
63
68
|
// 过滤掉 Form.Item 专用 props,只保留要传给子组件的
|
|
64
69
|
const childProps = Object.fromEntries(
|
|
65
|
-
Object.entries(
|
|
70
|
+
Object.entries(forwardedRest).filter(([key]) => !formItemPropKeys.includes(key as keyof ExtendedFormItemProps)),
|
|
66
71
|
);
|
|
67
72
|
|
|
68
73
|
const processedChildren =
|
|
@@ -74,7 +79,7 @@ export const FormItem = ({
|
|
|
74
79
|
}
|
|
75
80
|
return child;
|
|
76
81
|
});
|
|
77
|
-
const { label, labelWrap, colon = true, layout } =
|
|
82
|
+
const { label, labelWrap, colon = true, layout } = forwardedRest;
|
|
78
83
|
const effectiveLabelWrap = !layout || layout === 'vertical' ? true : labelWrap;
|
|
79
84
|
const labelColStyle =
|
|
80
85
|
layout === 'vertical' ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
|
|
@@ -122,17 +127,17 @@ export const FormItem = ({
|
|
|
122
127
|
};
|
|
123
128
|
return (
|
|
124
129
|
<Form.Item
|
|
125
|
-
{...
|
|
126
|
-
style={{ ...formItemStyle, ...
|
|
130
|
+
{...forwardedRest}
|
|
131
|
+
style={{ ...formItemStyle, ...forwardedRest.style }}
|
|
127
132
|
labelCol={{ style: labelColStyle }}
|
|
128
133
|
layout={layout}
|
|
129
134
|
label={renderLabel()}
|
|
130
135
|
colon={false}
|
|
131
|
-
extra={
|
|
136
|
+
extra={forwardedRest.extra && <span style={{ whiteSpace: 'pre-wrap' }}>{forwardedRest.extra}</span>}
|
|
132
137
|
tooltip={
|
|
133
|
-
|
|
138
|
+
forwardedRest.tooltip &&
|
|
134
139
|
({
|
|
135
|
-
title:
|
|
140
|
+
title: forwardedRest.tooltip,
|
|
136
141
|
overlayInnerStyle: { whiteSpace: 'pre-line' },
|
|
137
142
|
} as TooltipProps)
|
|
138
143
|
}
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
10
|
+
import { render } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { FormItem, formItemStyle, verticalFormItemLabelStyle } from '../FormItem';
|
|
12
14
|
|
|
13
15
|
describe('FormItem', () => {
|
|
14
16
|
it('keeps vertical label-to-value spacing consistent with v1', () => {
|
|
@@ -22,4 +24,17 @@ describe('FormItem', () => {
|
|
|
22
24
|
marginBottom: 12,
|
|
23
25
|
});
|
|
24
26
|
});
|
|
27
|
+
|
|
28
|
+
it('does not forward model-internal globalSort props to field children', () => {
|
|
29
|
+
const Field = vi.fn(() => <input aria-label="field" />);
|
|
30
|
+
|
|
31
|
+
render(
|
|
32
|
+
<FormItem globalSort={['title']} placeholder="Title">
|
|
33
|
+
<Field />
|
|
34
|
+
</FormItem>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(Field).toHaveBeenCalledWith(expect.not.objectContaining({ globalSort: expect.anything() }), {});
|
|
38
|
+
expect(Field).toHaveBeenCalledWith(expect.objectContaining({ placeholder: 'Title' }), {});
|
|
39
|
+
});
|
|
25
40
|
});
|