@nocobase/flow-engine 2.3.0-alpha.1 → 2.3.0-beta.2

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 (53) 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 +7 -1
  4. package/lib/components/MobilePopup.js +14 -3
  5. package/lib/components/subModel/LazyDropdown.js +62 -33
  6. package/lib/flowContext.d.ts +12 -1
  7. package/lib/flowContext.js +47 -6
  8. package/lib/locale/en-US.json +2 -0
  9. package/lib/locale/index.d.ts +4 -0
  10. package/lib/locale/zh-CN.json +2 -0
  11. package/lib/resources/flowResource.js +1 -0
  12. package/lib/utils/associationObjectVariable.d.ts +10 -0
  13. package/lib/utils/associationObjectVariable.js +10 -7
  14. package/lib/utils/dateVariable.d.ts +22 -0
  15. package/lib/utils/dateVariable.js +123 -16
  16. package/lib/utils/dirtyAwareApiClient.d.ts +1 -0
  17. package/lib/utils/dirtyAwareApiClient.js +15 -2
  18. package/lib/utils/index.d.ts +3 -3
  19. package/lib/utils/index.js +8 -0
  20. package/lib/utils/params-resolvers.d.ts +3 -0
  21. package/lib/utils/params-resolvers.js +10 -0
  22. package/lib/utils/variablesParams.js +5 -0
  23. package/lib/views/createViewMeta.d.ts +1 -0
  24. package/lib/views/createViewMeta.js +53 -22
  25. package/package.json +4 -4
  26. package/src/__tests__/createViewMeta.popup.test.ts +84 -1
  27. package/src/__tests__/flowContext.test.ts +8 -0
  28. package/src/__tests__/objectVariable.test.ts +6 -1
  29. package/src/__tests__/runjsFormSubmit.test.ts +138 -0
  30. package/src/acl/Acl.tsx +36 -1
  31. package/src/acl/__tests__/Acl.test.tsx +70 -0
  32. package/src/components/FlowContextSelector.tsx +7 -1
  33. package/src/components/MobilePopup.tsx +16 -4
  34. package/src/components/__tests__/MobilePopup.test.tsx +42 -1
  35. package/src/components/subModel/LazyDropdown.tsx +71 -38
  36. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +85 -2
  37. package/src/components/subModel/__tests__/LazyDropdown.test.tsx +202 -0
  38. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +35 -0
  39. package/src/flowContext.ts +79 -6
  40. package/src/locale/__tests__/index.test.ts +21 -0
  41. package/src/locale/en-US.json +2 -0
  42. package/src/locale/zh-CN.json +2 -0
  43. package/src/resources/__tests__/flowResource.test.ts +3 -0
  44. package/src/resources/flowResource.ts +1 -0
  45. package/src/utils/__tests__/dateVariable.test.ts +57 -4
  46. package/src/utils/__tests__/variablesParams.test.ts +28 -1
  47. package/src/utils/associationObjectVariable.ts +9 -6
  48. package/src/utils/dateVariable.ts +145 -18
  49. package/src/utils/dirtyAwareApiClient.ts +25 -2
  50. package/src/utils/index.ts +17 -2
  51. package/src/utils/params-resolvers.ts +12 -0
  52. package/src/utils/variablesParams.ts +10 -0
  53. 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) {
@@ -247,6 +247,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
247
247
  }, [active, cascaderProps.disabled, currentPath]);
248
248
  const handleChange = (0, import_react.useCallback)(
249
249
  (selectedValues, selectedOptions) => {
250
+ var _a;
250
251
  const lastOption = selectedOptions == null ? void 0 : selectedOptions[selectedOptions.length - 1];
251
252
  if (!selectedValues || selectedValues.length === 0) {
252
253
  onChange == null ? void 0 : onChange("", lastOption == null ? void 0 : lastOption.meta);
@@ -256,6 +257,7 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
256
257
  const path = selectedValues.map(String);
257
258
  const pathString = path.join(".");
258
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;
259
261
  const now = Date.now();
260
262
  let formattedValue;
261
263
  if (customFormatPathToValue) {
@@ -267,12 +269,16 @@ const FlowContextSelectorComponent = /* @__PURE__ */ __name(({
267
269
  formattedValue = (0, import_utils.formatPathToValue)(lastOption == null ? void 0 : lastOption.meta);
268
270
  }
269
271
  if (isLeaf) {
272
+ if (!isSelectable) {
273
+ setTempSelectedPath(path);
274
+ return;
275
+ }
270
276
  onChange == null ? void 0 : onChange(formattedValue, lastOption == null ? void 0 : lastOption.meta);
271
277
  setTempSelectedPath([]);
272
278
  return;
273
279
  }
274
280
  const lastSelected = lastSelectedRef.current;
275
- 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;
276
282
  if (isDoubleClick) {
277
283
  onChange == null ? void 0 : onChange(formattedValue, lastOption == null ? void 0 : lastOption.meta);
278
284
  lastSelectedRef.current = null;
@@ -47,25 +47,36 @@ 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) => {
51
58
  var _a;
52
59
  const { title, visible, onClose: closePopup, children, minHeight, className, footer } = props;
53
60
  const { t } = (0, import_react_i18next.useTranslation)();
54
61
  const { componentCls, hashId } = (0, import_MobilePopup.useMobileActionDrawerStyle)();
55
62
  const bodyStyles = (_a = props.styles) == null ? void 0 : _a.body;
63
+ const defaultMaxHeight = getMobilePopupMaxHeight();
56
64
  const popupStyle = (0, import_react.useMemo)(() => {
57
65
  return {
58
66
  minHeight: (bodyStyles == null ? void 0 : bodyStyles.minHeight) ?? minHeight,
59
67
  height: bodyStyles == null ? void 0 : bodyStyles.height,
60
- maxHeight: bodyStyles == null ? void 0 : bodyStyles.maxHeight
68
+ maxHeight: (bodyStyles == null ? void 0 : bodyStyles.maxHeight) ?? defaultMaxHeight
61
69
  };
62
- }, [bodyStyles == null ? void 0 : bodyStyles.height, bodyStyles == null ? void 0 : bodyStyles.maxHeight, bodyStyles == null ? void 0 : bodyStyles.minHeight, minHeight]);
70
+ }, [bodyStyles == null ? void 0 : bodyStyles.height, bodyStyles == null ? void 0 : bodyStyles.maxHeight, bodyStyles == null ? void 0 : bodyStyles.minHeight, defaultMaxHeight, minHeight]);
63
71
  const bodyStyle = (0, import_react.useMemo)(() => {
64
72
  return {
65
73
  padding: 0,
74
+ maxHeight: defaultMaxHeight,
75
+ overflowY: "auto",
76
+ overflowX: "hidden",
66
77
  ...bodyStyles
67
78
  };
68
- }, [bodyStyles]);
79
+ }, [bodyStyles, defaultMaxHeight]);
69
80
  const handleCloseKeyDown = (0, import_react.useCallback)(
70
81
  (event) => {
71
82
  if (event.key !== "Enter" && event.key !== " ") {
@@ -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()) {
@@ -24,9 +24,13 @@ import { FlowResource, FlowSQLRepository } from './resources';
24
24
  import type { ActionDefinition, EventDefinition, ResourceType } from './types';
25
25
  import { JSONValue } from './utils/params-resolvers';
26
26
  import type { RecordRef } from './utils/serverContextParams';
27
+ import { PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS } from './utils/dirtyAwareApiClient';
27
28
  import { FlowView, FlowViewer } from './views/FlowView';
28
29
  import { type RunJSVersion } from './runjs-context/registry';
29
30
  type Getter<T = any> = (ctx: FlowContext) => T | Promise<T>;
31
+ export type ResolveJsonTemplateOptions = {
32
+ contractModelUid?: string | number | null;
33
+ };
30
34
  export type FlowContextDocRef = string | {
31
35
  url: string;
32
36
  title?: string;
@@ -77,6 +81,7 @@ export interface MetaTreeNode {
77
81
  hidden?: boolean | (() => boolean);
78
82
  disabled?: boolean | (() => boolean);
79
83
  disabledReason?: string | (() => string | undefined);
84
+ selectable?: boolean;
80
85
  children?: MetaTreeNode[] | (() => Promise<MetaTreeNode[]>);
81
86
  }
82
87
  export interface PropertyMeta {
@@ -343,7 +348,7 @@ declare class BaseFlowEngineContext extends FlowContext {
343
348
  * @deprecated use `resolveJsonTemplate` instead
344
349
  */
345
350
  renderJson: (template: JSONValue) => Promise<any>;
346
- resolveJsonTemplate: (template: JSONValue) => Promise<any>;
351
+ resolveJsonTemplate: (template: JSONValue, options?: ResolveJsonTemplateOptions) => Promise<any>;
347
352
  getVar: (path: string) => Promise<any>;
348
353
  request: (options: RequestOptions) => Promise<any>;
349
354
  runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
@@ -469,6 +474,12 @@ export declare function createRunJSDeprecationProxy(ctx: any, options?: {
469
474
  doc?: RunJSDocMeta;
470
475
  }): any;
471
476
  export declare class FlowRunJSContext extends FlowContext {
477
+ [PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS](action: {
478
+ actionName: string;
479
+ dataSourceKey?: string;
480
+ resourceName: string;
481
+ resourceOf?: unknown;
482
+ }, params: Record<string, unknown> | undefined): Record<string, unknown>;
472
483
  constructor(delegate: FlowContext);
473
484
  exit(): void;
474
485
  exitAll(): void;
@@ -2383,8 +2383,8 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2383
2383
  this.defineMethod("renderJson", function(template) {
2384
2384
  return this.resolveJsonTemplate(template);
2385
2385
  });
2386
- this.defineMethod("resolveJsonTemplate", async function(template) {
2387
- var _a, _b;
2386
+ const resolveJsonTemplate = /* @__PURE__ */ __name(async function(template, options) {
2387
+ var _a, _b, _c;
2388
2388
  const used = (0, import_utils.extractUsedVariablePaths)(template);
2389
2389
  const usedVarNames = Object.keys(used || {});
2390
2390
  if (!usedVarNames.length) {
@@ -2412,13 +2412,13 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2412
2412
  let serverResolved = template;
2413
2413
  if (needServer) {
2414
2414
  const inferRecordRefWithMeta = /* @__PURE__ */ __name((ctx) => {
2415
- var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
2415
+ var _a2, _b2, _c2, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
2416
2416
  const ref = (0, import_variablesParams.inferRecordRef)(ctx);
2417
2417
  if (ref) return ref;
2418
2418
  try {
2419
2419
  const tk = (_b2 = (_a2 = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _a2.getMeta) == null ? void 0 : _b2.call(_a2, "currentFilterByTk");
2420
2420
  if (typeof tk === "undefined" || tk === null) return void 0;
2421
- const collection = ((_c = ctx == null ? void 0 : ctx.collection) == null ? void 0 : _c.name) || ((_j = (_i = (_h = (_g = (_f = (_e = (_d = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _d.getResourceName) == null ? void 0 : _e.call(_d)) == null ? void 0 : _f.split) == null ? void 0 : _g.call(_f, ".")) == null ? void 0 : _h.slice) == null ? void 0 : _i.call(_h, -1)) == null ? void 0 : _j[0]);
2421
+ const collection = ((_c2 = ctx == null ? void 0 : ctx.collection) == null ? void 0 : _c2.name) || ((_j = (_i = (_h = (_g = (_f = (_e = (_d = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _d.getResourceName) == null ? void 0 : _e.call(_d)) == null ? void 0 : _f.split) == null ? void 0 : _g.call(_f, ".")) == null ? void 0 : _h.slice) == null ? void 0 : _i.call(_h, -1)) == null ? void 0 : _j[0]);
2422
2422
  if (!collection) return void 0;
2423
2423
  const dataSourceKey = ((_k = ctx == null ? void 0 : ctx.collection) == null ? void 0 : _k.dataSourceKey) || ((_m = (_l = ctx == null ? void 0 : ctx.resource) == null ? void 0 : _l.getDataSourceKey) == null ? void 0 : _m.call(_l));
2424
2424
  return { collection, dataSourceKey, filterByTk: tk };
@@ -2461,6 +2461,11 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2461
2461
  }, "collectFromMeta");
2462
2462
  const inputFromMeta = await collectFromMeta();
2463
2463
  const autoInput = { ...inputFromMeta };
2464
+ const viewPaths = serverVarPaths.view || [];
2465
+ if (!autoInput.view && viewPaths.some((path) => path === "record" || path.startsWith("record.") || path.startsWith("record["))) {
2466
+ const recordRef = (0, import_variablesParams.inferViewRecordRef)(this);
2467
+ if (recordRef) autoInput.view = { record: recordRef };
2468
+ }
2464
2469
  try {
2465
2470
  const varName = "formValues";
2466
2471
  const neededPaths = serverVarPaths[varName] || [];
@@ -2511,18 +2516,25 @@ const _FlowEngineContext = class _FlowEngineContext extends BaseFlowEngineContex
2511
2516
  }
2512
2517
  if (this.api) {
2513
2518
  try {
2519
+ const contractRd = (0, import_params_resolvers.buildFlowModelResolveDescriptor)(
2520
+ this,
2521
+ options == null ? void 0 : options.contractModelUid
2522
+ );
2514
2523
  serverResolved = await (0, import_params_resolvers.enqueueVariablesResolve)(this, {
2524
+ ...contractRd ? { contractRd } : {},
2525
+ rd: (0, import_params_resolvers.buildFlowModelResolveDescriptor)(this, (_a = this.model) == null ? void 0 : _a.uid),
2515
2526
  template,
2516
2527
  contextParams: autoContextParams || {}
2517
2528
  });
2518
2529
  } catch (e) {
2519
- (_b = (_a = this.logger) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, { err: e }, "variables:resolve failed, fallback to client-only");
2530
+ (_c = (_b = this.logger) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, { err: e }, "variables:resolve failed, fallback to client-only");
2520
2531
  serverResolved = template;
2521
2532
  }
2522
2533
  }
2523
2534
  }
2524
2535
  return (0, import_utils.resolveExpressions)(serverResolved, this);
2525
- });
2536
+ }, "resolveJsonTemplate");
2537
+ this.defineMethod("resolveJsonTemplate", resolveJsonTemplate);
2526
2538
  this.defineMethod(
2527
2539
  "getVar",
2528
2540
  async function(varPath) {
@@ -3498,9 +3510,38 @@ function __mergeRunJSDocMeta(base, patch) {
3498
3510
  }
3499
3511
  __name(__mergeRunJSDocMeta, "__mergeRunJSDocMeta");
3500
3512
  const _FlowRunJSContext = class _FlowRunJSContext extends FlowContext {
3513
+ [import_dirtyAwareApiClient.PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS](action, params) {
3514
+ var _a, _b, _c, _d, _e;
3515
+ if (action.actionName.toLowerCase() !== "create" || !params || Array.isArray(params) || Object.prototype.hasOwnProperty.call(params, "updateAssociationValues") || !this.form || typeof ((_a = this.blockModel) == null ? void 0 : _a.submitFromRunJs) !== "function") {
3516
+ return params;
3517
+ }
3518
+ const resource = this.resource;
3519
+ const currentResourceName = (_b = resource == null ? void 0 : resource.getResourceName) == null ? void 0 : _b.call(resource);
3520
+ const currentDataSourceKey = ((_c = resource == null ? void 0 : resource.getDataSourceKey) == null ? void 0 : _c.call(resource)) || "main";
3521
+ if (action.resourceName !== currentResourceName || (action.dataSourceKey || "main") !== currentDataSourceKey) {
3522
+ return params;
3523
+ }
3524
+ const currentSourceId = (_d = resource == null ? void 0 : resource.getSourceId) == null ? void 0 : _d.call(resource);
3525
+ if ((currentResourceName == null ? void 0 : currentResourceName.includes(".")) && currentSourceId !== null && typeof currentSourceId !== "undefined" && String(action.resourceOf ?? "") !== String(currentSourceId)) {
3526
+ return params;
3527
+ }
3528
+ const updateAssociationValues = (_e = resource == null ? void 0 : resource.getUpdateAssociationValues) == null ? void 0 : _e.call(resource);
3529
+ if (!Array.isArray(updateAssociationValues) || updateAssociationValues.length === 0) {
3530
+ return params;
3531
+ }
3532
+ return {
3533
+ ...params,
3534
+ updateAssociationValues: [...updateAssociationValues]
3535
+ };
3536
+ }
3501
3537
  constructor(delegate) {
3538
+ var _a, _b;
3502
3539
  super();
3503
3540
  this.addDelegate(delegate);
3541
+ const submit = (_b = (_a = delegate.blockModel) == null ? void 0 : _a.submitFromRunJs) == null ? void 0 : _b.bind(delegate.blockModel);
3542
+ if (delegate.form && submit) {
3543
+ this.defineProperty("form", { value: { ...delegate.form, submit } });
3544
+ }
3504
3545
  this.defineProperty("React", { value: import_react.default });
3505
3546
  this.defineProperty("antd", { value: antd });
3506
3547
  this.defineProperty("dayjs", {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "Add": "Add",
3
+ "Are you sure you want to perform the action?": "Are you sure you want to perform the action?",
3
4
  "Are you sure you want to delete this item? This action cannot be undone.": "Are you sure you want to delete this item? This action cannot be undone.",
4
5
  "Are you sure to convert this template block to copy mode?": "Are you sure you want to convert this template block to copy mode?",
5
6
  "Array index out of bounds": "Array index {{index}} out of bounds for '{{subKey}}'",
@@ -53,6 +54,7 @@
53
54
  "Other blocks": "Other blocks",
54
55
  "Parent not found, cannot replace block": "Parent not found, cannot replace block",
55
56
  "Previous step": "Previous step",
57
+ "Please Confirm": "Please Confirm",
56
58
  "Replace current block with template?": "Replace current block with template?",
57
59
  "Replaced with template block": "Replaced with template block",
58
60
  "Render failed": "Render failed",
@@ -9,6 +9,7 @@
9
9
  export declare const locales: {
10
10
  'en-US': {
11
11
  Add: string;
12
+ "Are you sure you want to perform the action?": string;
12
13
  "Are you sure you want to delete this item? This action cannot be undone.": string;
13
14
  "Are you sure to convert this template block to copy mode?": string;
14
15
  "Array index out of bounds": string;
@@ -62,6 +63,7 @@ export declare const locales: {
62
63
  "Other blocks": string;
63
64
  "Parent not found, cannot replace block": string;
64
65
  "Previous step": string;
66
+ "Please Confirm": string;
65
67
  "Replace current block with template?": string;
66
68
  "Replaced with template block": string;
67
69
  "Render failed": string;
@@ -91,6 +93,7 @@ export declare const locales: {
91
93
  };
92
94
  'zh-CN': {
93
95
  Add: string;
96
+ "Are you sure you want to perform the action?": string;
94
97
  "Are you sure you want to delete this item? This action cannot be undone.": string;
95
98
  "Are you sure to convert this template block to copy mode?": string;
96
99
  "Array index out of bounds": string;
@@ -150,6 +153,7 @@ export declare const locales: {
150
153
  OK: string;
151
154
  "Other blocks": string;
152
155
  "Previous step": string;
156
+ "Please Confirm": string;
153
157
  "Render failed": string;
154
158
  "Response record": string;
155
159
  "Step configuration": string;
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "Add": "添加",
3
+ "Are you sure you want to perform the action?": "确定要执行此操作吗?",
3
4
  "Are you sure you want to delete this item? This action cannot be undone.": "确定要删除此项吗?此操作不可撤销。",
4
5
  "Are you sure to convert this template block to copy mode?": "确定将该模板区块转换为复制模式吗?",
5
6
  "Array index out of bounds": "数组索引 {{index}} 超出 '{{subKey}}' 的边界",
@@ -59,6 +60,7 @@
59
60
  "OK": "确定",
60
61
  "Other blocks": "其他区块",
61
62
  "Previous step": "上一步",
63
+ "Please Confirm": "请确认",
62
64
  "Render failed": "渲染失败",
63
65
  "Response record": "响应结果记录",
64
66
  "Step configuration": "步骤配置",
@@ -60,6 +60,7 @@ const _ResourceError = class _ResourceError extends Error {
60
60
  constructor(error) {
61
61
  const data = toErrMessages(error).shift();
62
62
  super(data.message);
63
+ this.data = data;
63
64
  this.name = "ResponseError";
64
65
  }
65
66
  get code() {
@@ -8,6 +8,16 @@
8
8
  */
9
9
  import type { Collection } from '../data-source';
10
10
  import type { FlowContext, PropertyMetaFactory } from '../flowContext';
11
+ /**
12
+ * 从值中提取主键:
13
+ * - 支持主键原始值(string/number)
14
+ * - 支持对象(按主键名取值)
15
+ *
16
+ * @param value 字段当前值
17
+ * @param primaryKey 主键字段名
18
+ * @returns 解析出的主键值,无法解析时返回 undefined
19
+ */
20
+ export declare function getAssociationFilterByTk(value: unknown, primaryKey: string | string[]): unknown;
11
21
  /**
12
22
  * 创建一个用于“对象类变量”(如 formValues / item)的 `resolveOnServer` 判定函数。
13
23
  * 仅当访问路径以“关联字段名”开头(且继续访问其子属性)时,返回 true 交由服务端解析;