@nocobase/flow-engine 2.2.0-beta.9 → 2.2.1

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.
Files changed (83) hide show
  1. package/lib/acl/Acl.d.ts +2 -1
  2. package/lib/acl/Acl.js +28 -0
  3. package/lib/components/FlowContextSelector.js +62 -13
  4. package/lib/components/FormItem.js +11 -7
  5. package/lib/components/MobilePopup.js +39 -10
  6. package/lib/components/MobilePopup.style.js +11 -1
  7. package/lib/components/subModel/LazyDropdown.js +62 -33
  8. package/lib/components/variables/VariableHybridInput.d.ts +9 -0
  9. package/lib/components/variables/VariableHybridInput.js +146 -17
  10. package/lib/components/variables/VariableInput.js +19 -7
  11. package/lib/components/variables/VariableTag.js +48 -36
  12. package/lib/components/variables/types.d.ts +21 -0
  13. package/lib/flowContext.d.ts +12 -1
  14. package/lib/flowContext.js +47 -6
  15. package/lib/flowEngine.js +6 -0
  16. package/lib/flowI18n.js +3 -3
  17. package/lib/locale/en-US.json +2 -0
  18. package/lib/locale/index.d.ts +4 -0
  19. package/lib/locale/zh-CN.json +2 -0
  20. package/lib/resources/flowResource.js +1 -0
  21. package/lib/types.d.ts +3 -1
  22. package/lib/types.js +1 -0
  23. package/lib/utils/associationObjectVariable.d.ts +10 -0
  24. package/lib/utils/associationObjectVariable.js +10 -7
  25. package/lib/utils/dateVariable.d.ts +22 -0
  26. package/lib/utils/dateVariable.js +123 -16
  27. package/lib/utils/dirtyAwareApiClient.d.ts +1 -0
  28. package/lib/utils/dirtyAwareApiClient.js +280 -13
  29. package/lib/utils/index.d.ts +3 -3
  30. package/lib/utils/index.js +8 -0
  31. package/lib/utils/loadedPageCache.d.ts +1 -0
  32. package/lib/utils/loadedPageCache.js +6 -0
  33. package/lib/utils/params-resolvers.d.ts +3 -0
  34. package/lib/utils/params-resolvers.js +10 -0
  35. package/lib/utils/variablesParams.js +5 -0
  36. package/lib/views/createViewMeta.d.ts +1 -0
  37. package/lib/views/createViewMeta.js +53 -22
  38. package/package.json +4 -4
  39. package/src/__tests__/createViewMeta.popup.test.ts +84 -1
  40. package/src/__tests__/flowContext.test.ts +8 -0
  41. package/src/__tests__/flowI18n.test.ts +11 -0
  42. package/src/__tests__/objectVariable.test.ts +6 -1
  43. package/src/__tests__/runjsFormSubmit.test.ts +138 -0
  44. package/src/__tests__/viewScopedFlowEngine.test.ts +72 -6
  45. package/src/acl/Acl.tsx +36 -1
  46. package/src/acl/__tests__/Acl.test.tsx +70 -0
  47. package/src/components/FlowContextSelector.tsx +73 -12
  48. package/src/components/FormItem.tsx +12 -7
  49. package/src/components/MobilePopup.style.ts +12 -1
  50. package/src/components/MobilePopup.tsx +42 -10
  51. package/src/components/__tests__/FormItem.test.tsx +17 -2
  52. package/src/components/__tests__/MobilePopup.test.tsx +150 -0
  53. package/src/components/subModel/LazyDropdown.tsx +71 -38
  54. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +85 -2
  55. package/src/components/subModel/__tests__/LazyDropdown.test.tsx +202 -0
  56. package/src/components/variables/VariableHybridInput.tsx +185 -14
  57. package/src/components/variables/VariableInput.tsx +32 -7
  58. package/src/components/variables/VariableTag.tsx +51 -37
  59. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +95 -3
  60. package/src/components/variables/__tests__/VariableHybridInput.test.tsx +212 -0
  61. package/src/components/variables/__tests__/VariableInput.test.tsx +202 -6
  62. package/src/components/variables/__tests__/VariableTag.test.tsx +80 -0
  63. package/src/components/variables/types.ts +21 -0
  64. package/src/flowContext.ts +79 -6
  65. package/src/flowEngine.ts +6 -0
  66. package/src/flowI18n.ts +8 -3
  67. package/src/locale/__tests__/index.test.ts +21 -0
  68. package/src/locale/en-US.json +2 -0
  69. package/src/locale/zh-CN.json +2 -0
  70. package/src/resources/__tests__/flowResource.test.ts +3 -0
  71. package/src/resources/flowResource.ts +1 -0
  72. package/src/types.ts +2 -0
  73. package/src/utils/__tests__/dateVariable.test.ts +57 -4
  74. package/src/utils/__tests__/dirtyAwareApiClient.test.ts +321 -0
  75. package/src/utils/__tests__/variablesParams.test.ts +28 -1
  76. package/src/utils/associationObjectVariable.ts +9 -6
  77. package/src/utils/dateVariable.ts +145 -18
  78. package/src/utils/dirtyAwareApiClient.ts +348 -13
  79. package/src/utils/index.ts +17 -2
  80. package/src/utils/loadedPageCache.ts +7 -0
  81. package/src/utils/params-resolvers.ts +12 -0
  82. package/src/utils/variablesParams.ts +10 -0
  83. package/src/views/createViewMeta.ts +52 -18
package/lib/acl/Acl.d.ts CHANGED
@@ -5,7 +5,7 @@ interface CheckOptions {
5
5
  actionName: string;
6
6
  fields?: string[];
7
7
  recordPkValue?: string | number;
8
- allowedActions: any[];
8
+ allowedActions?: Record<string, Array<string | number>>;
9
9
  }
10
10
  export declare class ACL {
11
11
  private flowEngine;
@@ -26,6 +26,7 @@ export declare class ACL {
26
26
  verifyScope: (actionName: string, recordPkValue: any, allowedActions: any) => boolean;
27
27
  parseAction(options: CheckOptions): any;
28
28
  parseField(options: CheckOptions): boolean;
29
+ can(options: CheckOptions): boolean;
29
30
  aclCheck(options: CheckOptions): Promise<boolean>;
30
31
  }
31
32
  export {};
package/lib/acl/Acl.js CHANGED
@@ -154,6 +154,34 @@ const _ACL = class _ACL {
154
154
  const allowed = whitelist.includes(fields[0]);
155
155
  return allowed;
156
156
  }
157
+ can(options) {
158
+ var _a;
159
+ const { allowAll } = this.data;
160
+ if (allowAll) {
161
+ return true;
162
+ }
163
+ const { actionName, allowedActions, recordPkValue } = options;
164
+ const hasRecordPkValue = recordPkValue !== void 0 && recordPkValue !== null;
165
+ const recordPermission = hasRecordPkValue && allowedActions ? this.verifyScope(actionName, recordPkValue, allowedActions) : null;
166
+ if (hasRecordPkValue && allowedActions && recordPermission !== true) {
167
+ return false;
168
+ }
169
+ const params = this.parseAction(options);
170
+ if (!params) {
171
+ return false;
172
+ }
173
+ if (!import_lodash.default.isEmpty(params.filter) && recordPermission !== true) {
174
+ return false;
175
+ }
176
+ if (!((_a = options.fields) == null ? void 0 : _a.length)) {
177
+ return true;
178
+ }
179
+ const allowedFields = [].concat(params.whitelist || []).concat(params.fields || []).concat(params.appends || []);
180
+ if (!allowedFields.length) {
181
+ return true;
182
+ }
183
+ return options.fields.every((field) => allowedFields.includes(field));
184
+ }
157
185
  async aclCheck(options) {
158
186
  const { allowAll } = this.data;
159
187
  if (allowAll) {
@@ -57,6 +57,15 @@ const cascaderPopupAutoHeightClassName = import_css.css`
57
57
  max-height: 50vh;
58
58
  }
59
59
  `;
60
+ function getMetaNodeTooltip(meta) {
61
+ if (!meta) {
62
+ return void 0;
63
+ }
64
+ const metaWithTooltip = meta;
65
+ const options = meta.options;
66
+ return metaWithTooltip.tooltip ?? (options == null ? void 0 : options.tooltip);
67
+ }
68
+ __name(getMetaNodeTooltip, "getMetaNodeTooltip");
60
69
  const normalizePath = /* @__PURE__ */ __name((path) => {
61
70
  if (!Array.isArray(path)) {
62
71
  return void 0;
@@ -90,6 +99,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
90
99
  value,
91
100
  onChange,
92
101
  children,
102
+ active,
93
103
  metaTree,
94
104
  showSearch = false,
95
105
  parseValueToPath: customParseValueToPath = import_utils.parseValueToPath,
@@ -97,6 +107,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
97
107
  open,
98
108
  onlyLeafSelectable = false,
99
109
  ignoreFieldNames,
110
+ dropdownFooter,
100
111
  ...cascaderProps
101
112
  }) => {
102
113
  const { token } = import_antd.theme.useToken();
@@ -111,15 +122,27 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
111
122
  const disabled = meta ? !!(typeof meta.disabled === "function" ? meta.disabled() : meta.disabled) : false;
112
123
  const disabledReason = meta ? typeof meta.disabledReason === "function" ? meta.disabledReason() : meta.disabledReason : void 0;
113
124
  const baseLabel = typeof o.label === "string" ? flowCtx.t(o.label) : o.label;
114
- const label = disabled ? /* @__PURE__ */ import_react.default.createElement("span", null, baseLabel, /* @__PURE__ */ import_react.default.createElement(
125
+ const labelText = typeof baseLabel === "string" ? baseLabel : String(o.value);
126
+ const tooltip = getMetaNodeTooltip(meta);
127
+ const tooltipTitle = disabled ? disabledReason || tooltip || flowCtx.t("This variable is not available") : tooltip;
128
+ const label = tooltipTitle ? /* @__PURE__ */ import_react.default.createElement("span", null, baseLabel, /* @__PURE__ */ import_react.default.createElement(
115
129
  import_antd.Tooltip,
116
130
  {
117
- title: disabledReason || flowCtx.t("This variable is not available"),
118
- placement: "right",
119
- overlayClassName: "flow-variable-disabled-tip",
131
+ title: typeof tooltipTitle === "string" ? flowCtx.t(tooltipTitle) : tooltipTitle,
132
+ placement: "top",
133
+ classNames: { root: "flow-variable-tip" },
120
134
  destroyTooltipOnHide: true
121
135
  },
122
- /* @__PURE__ */ import_react.default.createElement(import_icons.QuestionCircleOutlined, { style: { marginLeft: 6, color: "rgba(0,0,0,0.35)" } })
136
+ /* @__PURE__ */ import_react.default.createElement(
137
+ import_icons.QuestionCircleOutlined,
138
+ {
139
+ "aria-label": `${labelText} tooltip`,
140
+ style: {
141
+ marginLeft: token.marginXXS,
142
+ color: disabled ? token.colorTextDisabled : token.colorTextDescription
143
+ }
144
+ }
145
+ )
123
146
  )) : baseLabel;
124
147
  return {
125
148
  ...o,
@@ -129,7 +152,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
129
152
  };
130
153
  });
131
154
  },
132
- [flowCtx]
155
+ [flowCtx, token.colorTextDescription, token.colorTextDisabled, token.marginXXS]
133
156
  );
134
157
  const [updateFlag, setUpdateFlag] = (0, import_react.useState)(0);
135
158
  const [searchText, setSearchText] = (0, import_react.useState)("");
@@ -219,11 +242,12 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
219
242
  (0, import_utils.preloadContextSelectorPath)(options, pathToPreload, triggerUpdate);
220
243
  }, [options, pathToPreload, triggerUpdate]);
221
244
  const defaultChildren = (0, import_react.useMemo)(() => {
222
- const hasSelected = currentPath && currentPath.length > 0;
223
- return /* @__PURE__ */ import_react.default.createElement(import_antd.Button, { type: hasSelected ? "primary" : "default", style: defaultButtonStyle }, "x");
224
- }, [currentPath]);
245
+ const hasSelected = active ?? Boolean(currentPath && currentPath.length > 0);
246
+ return /* @__PURE__ */ import_react.default.createElement(import_antd.Button, { type: hasSelected ? "primary" : "default", style: defaultButtonStyle, disabled: cascaderProps.disabled }, "x");
247
+ }, [active, cascaderProps.disabled, currentPath]);
225
248
  const handleChange = (0, import_react.useCallback)(
226
249
  (selectedValues, selectedOptions) => {
250
+ var _a;
227
251
  const lastOption = selectedOptions == null ? void 0 : selectedOptions[selectedOptions.length - 1];
228
252
  if (!selectedValues || selectedValues.length === 0) {
229
253
  onChange == null ? void 0 : onChange("", lastOption == null ? void 0 : lastOption.meta);
@@ -233,6 +257,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
233
257
  const path = selectedValues.map(String);
234
258
  const pathString = path.join(".");
235
259
  const isLeaf = lastOption == null ? void 0 : lastOption.isLeaf;
260
+ const isSelectable = ((_a = lastOption == null ? void 0 : lastOption.meta) == null ? void 0 : _a.selectable) !== false;
236
261
  const now = Date.now();
237
262
  let formattedValue;
238
263
  if (customFormatPathToValue) {
@@ -244,12 +269,16 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
244
269
  formattedValue = (0, import_utils.formatPathToValue)(lastOption == null ? void 0 : lastOption.meta);
245
270
  }
246
271
  if (isLeaf) {
272
+ if (!isSelectable) {
273
+ setTempSelectedPath(path);
274
+ return;
275
+ }
247
276
  onChange == null ? void 0 : onChange(formattedValue, lastOption == null ? void 0 : lastOption.meta);
248
277
  setTempSelectedPath([]);
249
278
  return;
250
279
  }
251
280
  const lastSelected = lastSelectedRef.current;
252
- const isDoubleClick = !onlyLeafSelectable && (lastSelected == null ? void 0 : lastSelected.path) === pathString && now - lastSelected.time < 300;
281
+ const isDoubleClick = isSelectable && !onlyLeafSelectable && (lastSelected == null ? void 0 : lastSelected.path) === pathString && now - lastSelected.time < 300;
253
282
  if (isDoubleClick) {
254
283
  onChange == null ? void 0 : onChange(formattedValue, lastOption == null ? void 0 : lastOption.meta);
255
284
  lastSelectedRef.current = null;
@@ -289,12 +318,32 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
289
318
  },
290
319
  [cascaderOnDropdownVisibleChange, open]
291
320
  );
321
+ const footerNode = (0, import_react.useMemo)(() => {
322
+ if (dropdownFooter !== void 0) {
323
+ return dropdownFooter;
324
+ }
325
+ if (onlyLeafSelectable) {
326
+ return null;
327
+ }
328
+ return /* @__PURE__ */ import_react.default.createElement(
329
+ "div",
330
+ {
331
+ className: import_css.css`
332
+ padding: 6px 12px;
333
+ color: ${token.colorTextDescription};
334
+ border-top: 1px solid ${token.colorSplit};
335
+ font-size: ${token.fontSizeSM}px;
336
+ `
337
+ },
338
+ flowCtx.t("Double click to choose entire object")
339
+ );
340
+ }, [dropdownFooter, onlyLeafSelectable, token, flowCtx]);
292
341
  const renderDropdown = (0, import_react.useCallback)(
293
342
  (menu) => {
294
343
  const cascaderMenuNode = cascaderDropdownRender ? cascaderDropdownRender(menu) : menu;
295
344
  const cascaderMenu = import_react.default.isValidElement(cascaderMenuNode) ? cascaderMenuNode : /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, cascaderMenuNode);
296
345
  if (!isSearchEnabled || children === null) {
297
- return cascaderMenu;
346
+ return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, cascaderMenu, footerNode);
298
347
  }
299
348
  return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { className: cascaderSearchInputClassName }, /* @__PURE__ */ import_react.default.createElement(
300
349
  import_antd.Input,
@@ -306,9 +355,9 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
306
355
  onChange: (e) => setSearchText(e.target.value),
307
356
  onKeyDown: (e) => e.stopPropagation()
308
357
  }
309
- )), cascaderMenu);
358
+ )), cascaderMenu, footerNode);
310
359
  },
311
- [cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText]
360
+ [cascaderDropdownRender, cascaderSearchInputClassName, children, flowCtx, isSearchEnabled, searchText, footerNode]
312
361
  );
313
362
  const inlinePlaceholder = typeof restCascaderProps.placeholder === "string" ? restCascaderProps.placeholder : flowCtx.t("Search");
314
363
  const hasSelectedPath = Array.isArray(effectivePath) && effectivePath.length > 0;
@@ -77,14 +77,18 @@ const formItemPropKeys = [
77
77
  "required",
78
78
  "showLabel"
79
79
  ];
80
+ const modelInternalPropKeys = ["globalSort"];
80
81
  const FormItem = /* @__PURE__ */ __name(({
81
82
  children,
82
83
  showLabel = true,
83
84
  labelWidth,
84
85
  ...rest
85
86
  }) => {
87
+ const forwardedRest = Object.fromEntries(
88
+ Object.entries(rest).filter(([key]) => !modelInternalPropKeys.includes(key))
89
+ );
86
90
  const childProps = Object.fromEntries(
87
- Object.entries(rest).filter(([key]) => !formItemPropKeys.includes(key))
91
+ Object.entries(forwardedRest).filter(([key]) => !formItemPropKeys.includes(key))
88
92
  );
89
93
  const processedChildren = typeof children === "function" ? children : import_react.default.Children.map(children, (child) => {
90
94
  if (import_react.default.isValidElement(child)) {
@@ -92,7 +96,7 @@ const FormItem = /* @__PURE__ */ __name(({
92
96
  }
93
97
  return child;
94
98
  });
95
- const { label, labelWrap, colon = true, layout } = rest;
99
+ const { label, labelWrap, colon = true, layout } = forwardedRest;
96
100
  const effectiveLabelWrap = !layout || layout === "vertical" ? true : labelWrap;
97
101
  const labelColStyle = layout === "vertical" ? { width: labelWidth, ...verticalFormItemLabelStyle } : { width: labelWidth };
98
102
  const renderLabel = /* @__PURE__ */ __name(() => {
@@ -136,15 +140,15 @@ const FormItem = /* @__PURE__ */ __name(({
136
140
  return /* @__PURE__ */ import_react.default.createElement(
137
141
  import_antd.Form.Item,
138
142
  {
139
- ...rest,
140
- style: { ...formItemStyle, ...rest.style },
143
+ ...forwardedRest,
144
+ style: { ...formItemStyle, ...forwardedRest.style },
141
145
  labelCol: { style: labelColStyle },
142
146
  layout,
143
147
  label: renderLabel(),
144
148
  colon: false,
145
- extra: rest.extra && /* @__PURE__ */ import_react.default.createElement("span", { style: { whiteSpace: "pre-wrap" } }, rest.extra),
146
- tooltip: rest.tooltip && {
147
- title: rest.tooltip,
149
+ extra: forwardedRest.extra && /* @__PURE__ */ import_react.default.createElement("span", { style: { whiteSpace: "pre-wrap" } }, forwardedRest.extra),
150
+ tooltip: forwardedRest.tooltip && {
151
+ title: forwardedRest.tooltip,
148
152
  overlayInnerStyle: { whiteSpace: "pre-line" }
149
153
  }
150
154
  },
@@ -47,15 +47,46 @@ var import_react_i18next = require("react-i18next");
47
47
  var import_lazy_helper = require("../lazy-helper");
48
48
  const { Popup } = (0, import_lazy_helper.lazy)(() => import("antd-mobile"), "Popup");
49
49
  const { CloseOutline } = (0, import_lazy_helper.lazy)(() => import("antd-mobile-icons"), "CloseOutline");
50
+ const getMobilePopupMaxHeight = /* @__PURE__ */ __name(() => {
51
+ var _a;
52
+ if (typeof CSS !== "undefined" && ((_a = CSS.supports) == null ? void 0 : _a.call(CSS, "height", "100dvh"))) {
53
+ return "calc(100dvh - var(--nb-mobile-page-header-height, 46px))";
54
+ }
55
+ return "calc(100vh - var(--nb-mobile-page-header-height, 46px))";
56
+ }, "getMobilePopupMaxHeight");
50
57
  const MobilePopup = /* @__PURE__ */ __name((props) => {
58
+ var _a;
51
59
  const { title, visible, onClose: closePopup, children, minHeight, className, footer } = props;
52
60
  const { t } = (0, import_react_i18next.useTranslation)();
53
61
  const { componentCls, hashId } = (0, import_MobilePopup.useMobileActionDrawerStyle)();
54
- const style = (0, import_react.useMemo)(() => {
62
+ const bodyStyles = (_a = props.styles) == null ? void 0 : _a.body;
63
+ const defaultMaxHeight = getMobilePopupMaxHeight();
64
+ const popupStyle = (0, import_react.useMemo)(() => {
65
+ return {
66
+ minHeight: (bodyStyles == null ? void 0 : bodyStyles.minHeight) ?? minHeight,
67
+ height: bodyStyles == null ? void 0 : bodyStyles.height,
68
+ maxHeight: (bodyStyles == null ? void 0 : bodyStyles.maxHeight) ?? defaultMaxHeight
69
+ };
70
+ }, [bodyStyles == null ? void 0 : bodyStyles.height, bodyStyles == null ? void 0 : bodyStyles.maxHeight, bodyStyles == null ? void 0 : bodyStyles.minHeight, defaultMaxHeight, minHeight]);
71
+ const bodyStyle = (0, import_react.useMemo)(() => {
55
72
  return {
56
- minHeight
73
+ padding: 0,
74
+ maxHeight: defaultMaxHeight,
75
+ overflowY: "auto",
76
+ overflowX: "hidden",
77
+ ...bodyStyles
57
78
  };
58
- }, [minHeight]);
79
+ }, [bodyStyles, defaultMaxHeight]);
80
+ const handleCloseKeyDown = (0, import_react.useCallback)(
81
+ (event) => {
82
+ if (event.key !== "Enter" && event.key !== " ") {
83
+ return;
84
+ }
85
+ event.preventDefault();
86
+ closePopup();
87
+ },
88
+ [closePopup]
89
+ );
59
90
  const theme = (0, import_react.useMemo)(() => {
60
91
  return {
61
92
  token: {
@@ -75,21 +106,19 @@ const MobilePopup = /* @__PURE__ */ __name((props) => {
75
106
  onClose: closePopup,
76
107
  onMaskClick: closePopup,
77
108
  bodyClassName: "nb-mobile-action-drawer-body",
78
- bodyStyle: {
79
- padding: 0
80
- },
81
- maskStyle: style,
82
- style,
109
+ bodyStyle,
110
+ style: popupStyle,
83
111
  destroyOnClose: true
84
112
  },
85
- /* @__PURE__ */ import_react.default.createElement("div", { className: "nb-mobile-action-drawer-header" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "nb-mobile-action-drawer-placeholder" }, /* @__PURE__ */ import_react.default.createElement(CloseOutline, null)), /* @__PURE__ */ import_react.default.createElement("span", null, title), /* @__PURE__ */ import_react.default.createElement(
113
+ /* @__PURE__ */ import_react.default.createElement("div", { className: "nb-mobile-action-drawer-header" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "nb-mobile-action-drawer-placeholder" }, /* @__PURE__ */ import_react.default.createElement(CloseOutline, null)), /* @__PURE__ */ import_react.default.createElement("span", { className: "nb-mobile-action-drawer-title" }, title), /* @__PURE__ */ import_react.default.createElement(
86
114
  "span",
87
115
  {
88
116
  className: "nb-mobile-action-drawer-close-icon",
89
117
  onClick: closePopup,
90
118
  role: "button",
91
119
  tabIndex: 0,
92
- "aria-label": t("Close")
120
+ "aria-label": t("Close"),
121
+ onKeyDown: handleCloseKeyDown
93
122
  },
94
123
  /* @__PURE__ */ import_react.default.createElement(CloseOutline, null)
95
124
  )),
@@ -127,7 +127,7 @@ const useMobileActionDrawerStyle = genStyleHook("nb-mobile-action-drawer", (toke
127
127
  borderBottom: `1px solid ${token.colorSplit}`,
128
128
  position: "sticky",
129
129
  top: 0,
130
- backgroundColor: "white",
130
+ backgroundColor: token.colorBgContainer,
131
131
  zIndex: 1e3,
132
132
  // to match the button named 'Add block'
133
133
  "& + .nb-grid-container > .nb-grid > .nb-grid-warp > .ant-btn": {
@@ -137,11 +137,21 @@ const useMobileActionDrawerStyle = genStyleHook("nb-mobile-action-drawer", (toke
137
137
  ".nb-mobile-action-drawer-placeholder": {
138
138
  display: "inline-block",
139
139
  padding: 12,
140
+ flex: "0 0 auto",
140
141
  visibility: "hidden"
141
142
  },
143
+ ".nb-mobile-action-drawer-title": {
144
+ flex: "1 1 auto",
145
+ minWidth: 0,
146
+ overflow: "hidden",
147
+ textAlign: "center",
148
+ textOverflow: "ellipsis",
149
+ whiteSpace: "nowrap"
150
+ },
142
151
  ".nb-mobile-action-drawer-close-icon": {
143
152
  display: "inline-block",
144
153
  padding: 12,
154
+ flex: "0 0 auto",
145
155
  cursor: "pointer"
146
156
  },
147
157
  ".nb-mobile-action-drawer-body": {
@@ -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());
@@ -347,19 +341,15 @@ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchV
347
341
  e.stopPropagation();
348
342
  },
349
343
  onChange: (e) => {
350
- var _a;
351
344
  e.stopPropagation();
352
345
  const value = e.target.value;
353
346
  if (shouldActivateSearchSubmenu) {
354
347
  activateSearchSubmenu(searchKey);
355
348
  }
356
- if (((_a = e.nativeEvent) == null ? void 0 : _a.isComposing) || searchHandlers.isComposing(searchKey)) {
349
+ if (e.nativeEvent.isComposing || searchHandlers.isComposing(searchKey)) {
357
350
  searchHandlers.updateInputValue(searchKey, value);
358
351
  return;
359
352
  }
360
- if (!value && shouldActivateSearchSubmenu) {
361
- deactivateSearchSubmenu(searchKey);
362
- }
363
353
  searchHandlers.updateSearchValue(searchKey, value);
364
354
  },
365
355
  onCompositionStart: (e) => {
@@ -373,11 +363,7 @@ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchV
373
363
  e.stopPropagation();
374
364
  const value = e.currentTarget.value;
375
365
  if (shouldActivateSearchSubmenu) {
376
- if (value) {
377
- activateSearchSubmenu(searchKey);
378
- } else {
379
- deactivateSearchSubmenu(searchKey);
380
- }
366
+ activateSearchSubmenu(searchKey);
381
367
  }
382
368
  searchHandlers.endComposition(searchKey, value);
383
369
  },
@@ -385,10 +371,20 @@ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchV
385
371
  e.stopPropagation();
386
372
  },
387
373
  onKeyDown: (e) => {
374
+ if (e.key === "Escape" || e.key === "Tab") {
375
+ deactivateSearchSubmenu(searchKey);
376
+ return;
377
+ }
378
+ if (shouldActivateSearchSubmenu) {
379
+ activateSearchSubmenu(searchKey);
380
+ }
388
381
  e.stopPropagation();
389
382
  },
390
383
  onMouseDown: (e) => {
391
384
  e.stopPropagation();
385
+ if (shouldActivateSearchSubmenu) {
386
+ activateSearchSubmenu(searchKey);
387
+ }
392
388
  },
393
389
  size: "small",
394
390
  style: {
@@ -409,7 +405,8 @@ const KEEP_OPEN_LABEL_STYLE = {
409
405
  width: "100%"
410
406
  };
411
407
  const DROPDOWN_PERSIST_TTL_MS = 350;
412
- const SUBMENU_CLOSE_DELAY = 0.05;
408
+ const DEFAULT_DROPDOWN_MAX_HEIGHT = 400;
409
+ const MENU_CLOSE_DELAY = 0.3;
413
410
  const SUBMENU_MOTION_DISABLED = {
414
411
  motionEnter: false,
415
412
  motionLeave: false
@@ -417,14 +414,21 @@ const SUBMENU_MOTION_DISABLED = {
417
414
  const dropdownPersistRegistry = /* @__PURE__ */ new Map();
418
415
  const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
419
416
  const engine = (0, import_provider.useFlowEngine)();
417
+ const { getPrefixCls } = import_react.default.useContext(import_antd.ConfigProvider.ConfigContext);
418
+ const { token } = import_antd.theme.useToken();
419
+ const triggerId = import_react.default.useId();
420
+ const showArrow = Boolean(props.arrow);
420
421
  const [menuVisible, setMenuVisible] = (0, import_react.useState)(false);
422
+ const [dropdownMaxHeight, setDropdownMaxHeight] = (0, import_react.useState)(DEFAULT_DROPDOWN_MAX_HEIGHT);
421
423
  const [openKeys, setOpenKeys] = (0, import_react.useState)(/* @__PURE__ */ new Set());
422
- const [activeSearchKey, setActiveSearchKey] = (0, import_react.useState)(null);
423
424
  const [rootItems, setRootItems] = (0, import_react.useState)([]);
424
425
  const [rootLoading, setRootLoading] = (0, import_react.useState)(false);
426
+ const activeSearchKeyRef = (0, import_react.useRef)(null);
425
427
  const closeByOutsideClickRef = (0, import_react.useRef)(false);
426
428
  const skipPreserveActiveSearchRef = (0, import_react.useRef)(false);
427
- const dropdownMaxHeight = useNiceDropdownMaxHeight();
429
+ const triggerOpenClassName = `nb-lazy-dropdown-trigger-${triggerId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
430
+ const defaultOpenClassName = `${getPrefixCls("dropdown", props.prefixCls)}-open`;
431
+ const mergedOpenClassName = [props.openClassName ?? defaultOpenClassName, triggerOpenClassName].filter(Boolean).join(" ");
428
432
  const t = engine.translate.bind(engine);
429
433
  const { items: menuItems, keepDropdownOpen, persistKey, stateVersion, refreshKeys, ...dropdownMenuProps } = menu;
430
434
  const { loadedChildren, loadingKeys, handleLoadChildren } = useAsyncMenuItems(
@@ -438,14 +442,31 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
438
442
  const { searchValues, inputValues, clearSearchValue, clearAllSearchValues } = searchHandlers;
439
443
  const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
440
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]);
441
462
  const closeMenu = (0, import_react.useCallback)(() => {
442
463
  setMenuVisible(false);
443
- setActiveSearchKey(null);
464
+ activeSearchKeyRef.current = null;
444
465
  setOpenKeys(/* @__PURE__ */ new Set());
445
466
  clearAllSearchValues();
446
467
  }, [clearAllSearchValues]);
447
468
  const activateSearchSubmenu = (0, import_react.useCallback)((key) => {
448
- setActiveSearchKey(key);
469
+ activeSearchKeyRef.current = key;
449
470
  setOpenKeys((prev) => {
450
471
  if (prev.has(key)) return prev;
451
472
  const next = new Set(prev);
@@ -454,34 +475,38 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
454
475
  });
455
476
  }, []);
456
477
  const deactivateSearchSubmenu = (0, import_react.useCallback)((key) => {
457
- setActiveSearchKey((prev) => prev === key ? null : prev);
478
+ if (activeSearchKeyRef.current === key) {
479
+ activeSearchKeyRef.current = null;
480
+ }
458
481
  }, []);
459
482
  const closeActiveSearchForPath = (0, import_react.useCallback)(
460
483
  (keyPath) => {
484
+ const activeSearchKey = activeSearchKeyRef.current;
461
485
  if (!activeSearchKey || keyPath === activeSearchKey || keyPath.startsWith(`${activeSearchKey}/`) || activeSearchKey.startsWith(`${keyPath}/`)) {
462
486
  return;
463
487
  }
464
488
  skipPreserveActiveSearchRef.current = true;
465
489
  clearSearchValue(activeSearchKey);
466
- setActiveSearchKey(null);
490
+ activeSearchKeyRef.current = null;
467
491
  setOpenKeys((prev) => {
468
492
  const next = new Set(prev);
469
493
  next.delete(activeSearchKey);
470
494
  return next;
471
495
  });
472
496
  },
473
- [activeSearchKey, clearSearchValue]
497
+ [clearSearchValue]
474
498
  );
475
499
  const handleMenuOpenChange = (0, import_react.useCallback)(
476
500
  (nextOpenKeys) => {
477
501
  var _a, _b;
478
502
  let normalized = normalizeOpenKeys(nextOpenKeys);
479
- if (activeSearchKey && openKeys.has(activeSearchKey) && !normalized.includes(activeSearchKey)) {
480
- if (normalized.length || skipPreserveActiveSearchRef.current) {
503
+ const activeSearchKey = activeSearchKeyRef.current;
504
+ if (activeSearchKey && !normalized.includes(activeSearchKey)) {
505
+ if (skipPreserveActiveSearchRef.current) {
481
506
  clearSearchValue(activeSearchKey);
482
- setActiveSearchKey(null);
507
+ activeSearchKeyRef.current = null;
483
508
  } else {
484
- normalized = [activeSearchKey];
509
+ normalized = Array.from(openKeys);
485
510
  }
486
511
  }
487
512
  if (!normalized.length && shouldPreventClose()) {
@@ -498,13 +523,15 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
498
523
  (_b = dropdownMenuProps.onOpenChange) == null ? void 0 : _b.call(dropdownMenuProps, normalized);
499
524
  skipPreserveActiveSearchRef.current = false;
500
525
  },
501
- [activeSearchKey, clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose]
526
+ [clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose]
502
527
  );
503
528
  (0, import_react.useEffect)(() => {
504
529
  if (!menuVisible) return;
505
530
  const markOutsideClick = /* @__PURE__ */ __name((event) => {
506
531
  const target = event.target;
507
- const isOutside = !(target == null ? void 0 : target.closest(".ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup"));
532
+ const isInsidePopup = target == null ? void 0 : target.closest(".ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup");
533
+ const isInsideCurrentTrigger = target == null ? void 0 : target.closest(`.${triggerOpenClassName}`);
534
+ const isOutside = !isInsidePopup && !isInsideCurrentTrigger;
508
535
  closeByOutsideClickRef.current = isOutside;
509
536
  if (isOutside) {
510
537
  closeMenu();
@@ -516,7 +543,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
516
543
  document.removeEventListener("pointerdown", markOutsideClick, true);
517
544
  document.removeEventListener("mousedown", markOutsideClick, true);
518
545
  };
519
- }, [closeMenu, menuVisible]);
546
+ }, [closeMenu, menuVisible, triggerOpenClassName]);
520
547
  (0, import_react.useEffect)(() => {
521
548
  if (!persistKey) return;
522
549
  const until = dropdownPersistRegistry.get(persistKey) || 0;
@@ -734,13 +761,15 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
734
761
  ...props,
735
762
  open: menuVisible,
736
763
  destroyPopupOnHide: true,
764
+ mouseLeaveDelay: props.mouseLeaveDelay ?? MENU_CLOSE_DELAY,
765
+ openClassName: mergedOpenClassName,
737
766
  overlayClassName,
738
767
  placement: "bottomLeft",
739
768
  menu: {
740
769
  ...dropdownMenuProps,
741
770
  openKeys: Array.from(openKeys),
742
771
  items,
743
- subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? SUBMENU_CLOSE_DELAY,
772
+ subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? MENU_CLOSE_DELAY,
744
773
  motion: dropdownMenuProps.motion ?? SUBMENU_MOTION_DISABLED,
745
774
  onClick: /* @__PURE__ */ __name(() => {
746
775
  }, "onClick"),
@@ -752,7 +781,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
752
781
  }
753
782
  },
754
783
  onOpenChange: (visible, info) => {
755
- if (!visible && activeSearchKey && (info == null ? void 0 : info.source) === "trigger" && !closeByOutsideClickRef.current) {
784
+ if (!visible && activeSearchKeyRef.current && (info == null ? void 0 : info.source) === "trigger" && !closeByOutsideClickRef.current) {
756
785
  return;
757
786
  }
758
787
  if (!visible && shouldPreventClose()) {
@@ -17,11 +17,20 @@ export interface VariableHybridInputProps {
17
17
  value?: string;
18
18
  onChange?: (value: string) => void;
19
19
  disabled?: boolean;
20
+ readOnly?: boolean;
20
21
  placeholder?: string;
21
22
  addonBefore?: React.ReactNode;
22
23
  metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
23
24
  converters?: VariableHybridInputConverters;
24
25
  style?: React.CSSProperties;
25
26
  className?: string;
27
+ /**
28
+ * Validation status — turns the input border red (`error`) or amber
29
+ * (`warning`). Usually omitted: when rendered inside an antd `Form.Item`, the
30
+ * status is read automatically from `FormItemInputContext`, so dropping this
31
+ * into a `Form.Item` with failing rules colours the border with no extra
32
+ * wiring. An explicit prop wins over the inherited form status.
33
+ */
34
+ status?: 'error' | 'warning';
26
35
  }
27
36
  export declare const VariableHybridInput: React.NamedExoticComponent<VariableHybridInputProps>;