@nocobase/flow-engine 2.1.27 → 2.1.29

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.
@@ -347,19 +347,15 @@ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchV
347
347
  e.stopPropagation();
348
348
  },
349
349
  onChange: (e) => {
350
- var _a;
351
350
  e.stopPropagation();
352
351
  const value = e.target.value;
353
352
  if (shouldActivateSearchSubmenu) {
354
353
  activateSearchSubmenu(searchKey);
355
354
  }
356
- if (((_a = e.nativeEvent) == null ? void 0 : _a.isComposing) || searchHandlers.isComposing(searchKey)) {
355
+ if (e.nativeEvent.isComposing || searchHandlers.isComposing(searchKey)) {
357
356
  searchHandlers.updateInputValue(searchKey, value);
358
357
  return;
359
358
  }
360
- if (!value && shouldActivateSearchSubmenu) {
361
- deactivateSearchSubmenu(searchKey);
362
- }
363
359
  searchHandlers.updateSearchValue(searchKey, value);
364
360
  },
365
361
  onCompositionStart: (e) => {
@@ -373,11 +369,7 @@ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchV
373
369
  e.stopPropagation();
374
370
  const value = e.currentTarget.value;
375
371
  if (shouldActivateSearchSubmenu) {
376
- if (value) {
377
- activateSearchSubmenu(searchKey);
378
- } else {
379
- deactivateSearchSubmenu(searchKey);
380
- }
372
+ activateSearchSubmenu(searchKey);
381
373
  }
382
374
  searchHandlers.endComposition(searchKey, value);
383
375
  },
@@ -385,10 +377,20 @@ const createSearchItem = /* @__PURE__ */ __name((item, searchKey, currentSearchV
385
377
  e.stopPropagation();
386
378
  },
387
379
  onKeyDown: (e) => {
380
+ if (e.key === "Escape" || e.key === "Tab") {
381
+ deactivateSearchSubmenu(searchKey);
382
+ return;
383
+ }
384
+ if (shouldActivateSearchSubmenu) {
385
+ activateSearchSubmenu(searchKey);
386
+ }
388
387
  e.stopPropagation();
389
388
  },
390
389
  onMouseDown: (e) => {
391
390
  e.stopPropagation();
391
+ if (shouldActivateSearchSubmenu) {
392
+ activateSearchSubmenu(searchKey);
393
+ }
392
394
  },
393
395
  size: "small",
394
396
  style: {
@@ -409,7 +411,7 @@ const KEEP_OPEN_LABEL_STYLE = {
409
411
  width: "100%"
410
412
  };
411
413
  const DROPDOWN_PERSIST_TTL_MS = 350;
412
- const SUBMENU_CLOSE_DELAY = 0.05;
414
+ const MENU_CLOSE_DELAY = 0.3;
413
415
  const SUBMENU_MOTION_DISABLED = {
414
416
  motionEnter: false,
415
417
  motionLeave: false
@@ -419,9 +421,9 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
419
421
  const engine = (0, import_provider.useFlowEngine)();
420
422
  const [menuVisible, setMenuVisible] = (0, import_react.useState)(false);
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
429
  const dropdownMaxHeight = useNiceDropdownMaxHeight();
@@ -440,12 +442,12 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
440
442
  useSubmenuStyles(menuVisible, dropdownMaxHeight);
441
443
  const closeMenu = (0, import_react.useCallback)(() => {
442
444
  setMenuVisible(false);
443
- setActiveSearchKey(null);
445
+ activeSearchKeyRef.current = null;
444
446
  setOpenKeys(/* @__PURE__ */ new Set());
445
447
  clearAllSearchValues();
446
448
  }, [clearAllSearchValues]);
447
449
  const activateSearchSubmenu = (0, import_react.useCallback)((key) => {
448
- setActiveSearchKey(key);
450
+ activeSearchKeyRef.current = key;
449
451
  setOpenKeys((prev) => {
450
452
  if (prev.has(key)) return prev;
451
453
  const next = new Set(prev);
@@ -454,34 +456,38 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
454
456
  });
455
457
  }, []);
456
458
  const deactivateSearchSubmenu = (0, import_react.useCallback)((key) => {
457
- setActiveSearchKey((prev) => prev === key ? null : prev);
459
+ if (activeSearchKeyRef.current === key) {
460
+ activeSearchKeyRef.current = null;
461
+ }
458
462
  }, []);
459
463
  const closeActiveSearchForPath = (0, import_react.useCallback)(
460
464
  (keyPath) => {
465
+ const activeSearchKey = activeSearchKeyRef.current;
461
466
  if (!activeSearchKey || keyPath === activeSearchKey || keyPath.startsWith(`${activeSearchKey}/`) || activeSearchKey.startsWith(`${keyPath}/`)) {
462
467
  return;
463
468
  }
464
469
  skipPreserveActiveSearchRef.current = true;
465
470
  clearSearchValue(activeSearchKey);
466
- setActiveSearchKey(null);
471
+ activeSearchKeyRef.current = null;
467
472
  setOpenKeys((prev) => {
468
473
  const next = new Set(prev);
469
474
  next.delete(activeSearchKey);
470
475
  return next;
471
476
  });
472
477
  },
473
- [activeSearchKey, clearSearchValue]
478
+ [clearSearchValue]
474
479
  );
475
480
  const handleMenuOpenChange = (0, import_react.useCallback)(
476
481
  (nextOpenKeys) => {
477
482
  var _a, _b;
478
483
  let normalized = normalizeOpenKeys(nextOpenKeys);
479
- if (activeSearchKey && openKeys.has(activeSearchKey) && !normalized.includes(activeSearchKey)) {
480
- if (normalized.length || skipPreserveActiveSearchRef.current) {
484
+ const activeSearchKey = activeSearchKeyRef.current;
485
+ if (activeSearchKey && !normalized.includes(activeSearchKey)) {
486
+ if (skipPreserveActiveSearchRef.current) {
481
487
  clearSearchValue(activeSearchKey);
482
- setActiveSearchKey(null);
488
+ activeSearchKeyRef.current = null;
483
489
  } else {
484
- normalized = [activeSearchKey];
490
+ normalized = Array.from(openKeys);
485
491
  }
486
492
  }
487
493
  if (!normalized.length && shouldPreventClose()) {
@@ -498,7 +504,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
498
504
  (_b = dropdownMenuProps.onOpenChange) == null ? void 0 : _b.call(dropdownMenuProps, normalized);
499
505
  skipPreserveActiveSearchRef.current = false;
500
506
  },
501
- [activeSearchKey, clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose]
507
+ [clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose]
502
508
  );
503
509
  (0, import_react.useEffect)(() => {
504
510
  if (!menuVisible) return;
@@ -734,13 +740,14 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
734
740
  ...props,
735
741
  open: menuVisible,
736
742
  destroyPopupOnHide: true,
743
+ mouseLeaveDelay: props.mouseLeaveDelay ?? MENU_CLOSE_DELAY,
737
744
  overlayClassName,
738
745
  placement: "bottomLeft",
739
746
  menu: {
740
747
  ...dropdownMenuProps,
741
748
  openKeys: Array.from(openKeys),
742
749
  items,
743
- subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? SUBMENU_CLOSE_DELAY,
750
+ subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? MENU_CLOSE_DELAY,
744
751
  motion: dropdownMenuProps.motion ?? SUBMENU_MOTION_DISABLED,
745
752
  onClick: /* @__PURE__ */ __name(() => {
746
753
  }, "onClick"),
@@ -752,7 +759,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
752
759
  }
753
760
  },
754
761
  onOpenChange: (visible, info) => {
755
- if (!visible && activeSearchKey && (info == null ? void 0 : info.source) === "trigger" && !closeByOutsideClickRef.current) {
762
+ if (!visible && activeSearchKeyRef.current && (info == null ? void 0 : info.source) === "trigger" && !closeByOutsideClickRef.current) {
756
763
  return;
757
764
  }
758
765
  if (!visible && shouldPreventClose()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.1.27",
3
+ "version": "2.1.29",
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.1.27",
12
- "@nocobase/shared": "2.1.27",
11
+ "@nocobase/sdk": "2.1.29",
12
+ "@nocobase/shared": "2.1.29",
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": "c1311c48cf65f8eeac17411d3380ebdd0918f1e2"
40
+ "gitHead": "d9848c5df07cef806cb938bb14b3f4ef5f3d0129"
41
41
  }
@@ -484,13 +484,10 @@ const createSearchItem = (
484
484
  if (shouldActivateSearchSubmenu) {
485
485
  activateSearchSubmenu(searchKey);
486
486
  }
487
- if ((e.nativeEvent as any)?.isComposing || searchHandlers.isComposing(searchKey)) {
487
+ if ((e.nativeEvent as InputEvent).isComposing || searchHandlers.isComposing(searchKey)) {
488
488
  searchHandlers.updateInputValue(searchKey, value);
489
489
  return;
490
490
  }
491
- if (!value && shouldActivateSearchSubmenu) {
492
- deactivateSearchSubmenu(searchKey);
493
- }
494
491
  searchHandlers.updateSearchValue(searchKey, value);
495
492
  }}
496
493
  onCompositionStart={(e) => {
@@ -504,11 +501,7 @@ const createSearchItem = (
504
501
  e.stopPropagation();
505
502
  const value = e.currentTarget.value;
506
503
  if (shouldActivateSearchSubmenu) {
507
- if (value) {
508
- activateSearchSubmenu(searchKey);
509
- } else {
510
- deactivateSearchSubmenu(searchKey);
511
- }
504
+ activateSearchSubmenu(searchKey);
512
505
  }
513
506
  searchHandlers.endComposition(searchKey, value);
514
507
  }}
@@ -516,11 +509,21 @@ const createSearchItem = (
516
509
  e.stopPropagation();
517
510
  }}
518
511
  onKeyDown={(e) => {
512
+ if (e.key === 'Escape' || e.key === 'Tab') {
513
+ deactivateSearchSubmenu(searchKey);
514
+ return;
515
+ }
516
+ if (shouldActivateSearchSubmenu) {
517
+ activateSearchSubmenu(searchKey);
518
+ }
519
519
  e.stopPropagation();
520
520
  }}
521
521
  onMouseDown={(e) => {
522
522
  // 防止菜单聚焦丢失或页面滚动
523
523
  e.stopPropagation();
524
+ if (shouldActivateSearchSubmenu) {
525
+ activateSearchSubmenu(searchKey);
526
+ }
524
527
  }}
525
528
  size="small"
526
529
  style={{
@@ -552,7 +555,7 @@ const KEEP_OPEN_LABEL_STYLE: React.CSSProperties = {
552
555
 
553
556
  // 短暂保持打开状态的注册表(用于跨父节点快速重建时的恢复)
554
557
  const DROPDOWN_PERSIST_TTL_MS = 350;
555
- const SUBMENU_CLOSE_DELAY = 0.05;
558
+ const MENU_CLOSE_DELAY = 0.3;
556
559
  const SUBMENU_MOTION_DISABLED = {
557
560
  motionEnter: false,
558
561
  motionLeave: false,
@@ -563,9 +566,9 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
563
566
  const engine = useFlowEngine();
564
567
  const [menuVisible, setMenuVisible] = useState(false);
565
568
  const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
566
- const [activeSearchKey, setActiveSearchKey] = useState<string | null>(null);
567
569
  const [rootItems, setRootItems] = useState<Item[]>([]);
568
570
  const [rootLoading, setRootLoading] = useState(false);
571
+ const activeSearchKeyRef = useRef<string | null>(null);
569
572
  const closeByOutsideClickRef = useRef(false);
570
573
  const skipPreserveActiveSearchRef = useRef(false);
571
574
  const dropdownMaxHeight = useNiceDropdownMaxHeight();
@@ -589,13 +592,13 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
589
592
 
590
593
  const closeMenu = useCallback(() => {
591
594
  setMenuVisible(false);
592
- setActiveSearchKey(null);
595
+ activeSearchKeyRef.current = null;
593
596
  setOpenKeys(new Set());
594
597
  clearAllSearchValues();
595
598
  }, [clearAllSearchValues]);
596
599
 
597
600
  const activateSearchSubmenu = useCallback((key: string) => {
598
- setActiveSearchKey(key);
601
+ activeSearchKeyRef.current = key;
599
602
  setOpenKeys((prev) => {
600
603
  if (prev.has(key)) return prev;
601
604
  const next = new Set(prev);
@@ -605,11 +608,14 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
605
608
  }, []);
606
609
 
607
610
  const deactivateSearchSubmenu = useCallback((key: string) => {
608
- setActiveSearchKey((prev) => (prev === key ? null : prev));
611
+ if (activeSearchKeyRef.current === key) {
612
+ activeSearchKeyRef.current = null;
613
+ }
609
614
  }, []);
610
615
 
611
616
  const closeActiveSearchForPath = useCallback(
612
617
  (keyPath: string) => {
618
+ const activeSearchKey = activeSearchKeyRef.current;
613
619
  if (
614
620
  !activeSearchKey ||
615
621
  keyPath === activeSearchKey ||
@@ -621,25 +627,26 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
621
627
 
622
628
  skipPreserveActiveSearchRef.current = true;
623
629
  clearSearchValue(activeSearchKey);
624
- setActiveSearchKey(null);
630
+ activeSearchKeyRef.current = null;
625
631
  setOpenKeys((prev) => {
626
632
  const next = new Set(prev);
627
633
  next.delete(activeSearchKey);
628
634
  return next;
629
635
  });
630
636
  },
631
- [activeSearchKey, clearSearchValue],
637
+ [clearSearchValue],
632
638
  );
633
639
 
634
640
  const handleMenuOpenChange = useCallback(
635
641
  (nextOpenKeys: string[]) => {
636
642
  let normalized = normalizeOpenKeys(nextOpenKeys);
637
- if (activeSearchKey && openKeys.has(activeSearchKey) && !normalized.includes(activeSearchKey)) {
638
- if (normalized.length || skipPreserveActiveSearchRef.current) {
643
+ const activeSearchKey = activeSearchKeyRef.current;
644
+ if (activeSearchKey && !normalized.includes(activeSearchKey)) {
645
+ if (skipPreserveActiveSearchRef.current) {
639
646
  clearSearchValue(activeSearchKey);
640
- setActiveSearchKey(null);
647
+ activeSearchKeyRef.current = null;
641
648
  } else {
642
- normalized = [activeSearchKey];
649
+ normalized = Array.from(openKeys);
643
650
  }
644
651
  }
645
652
 
@@ -658,7 +665,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
658
665
  dropdownMenuProps.onOpenChange?.(normalized);
659
666
  skipPreserveActiveSearchRef.current = false;
660
667
  },
661
- [activeSearchKey, clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose],
668
+ [clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose],
662
669
  );
663
670
 
664
671
  useEffect(() => {
@@ -957,13 +964,14 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
957
964
  {...props}
958
965
  open={menuVisible}
959
966
  destroyPopupOnHide
967
+ mouseLeaveDelay={props.mouseLeaveDelay ?? MENU_CLOSE_DELAY}
960
968
  overlayClassName={overlayClassName}
961
969
  placement="bottomLeft"
962
970
  menu={{
963
971
  ...dropdownMenuProps,
964
972
  openKeys: Array.from(openKeys),
965
973
  items: items,
966
- subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? SUBMENU_CLOSE_DELAY,
974
+ subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? MENU_CLOSE_DELAY,
967
975
  motion: dropdownMenuProps.motion ?? SUBMENU_MOTION_DISABLED,
968
976
  onClick: () => {},
969
977
  onOpenChange: handleMenuOpenChange,
@@ -974,7 +982,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
974
982
  },
975
983
  }}
976
984
  onOpenChange={(visible, info) => {
977
- if (!visible && activeSearchKey && info?.source === 'trigger' && !closeByOutsideClickRef.current) {
985
+ if (!visible && activeSearchKeyRef.current && info?.source === 'trigger' && !closeByOutsideClickRef.current) {
978
986
  return;
979
987
  }
980
988
 
@@ -411,7 +411,7 @@ describe('transformItems - searchable flags', () => {
411
411
  await waitFor(() => expect(screen.getAllByText('No data').length).toBeGreaterThan(0));
412
412
  });
413
413
 
414
- it('closes searchable submenu after focus without input', async () => {
414
+ it('keeps searchable submenu open while interacting with its input', async () => {
415
415
  const engine = new FlowEngine();
416
416
  await engine.flowSettings.forceEnable();
417
417
  class Parent extends FlowModel {}
@@ -451,7 +451,11 @@ describe('transformItems - searchable flags', () => {
451
451
  await user.click(screen.getByRole('textbox'));
452
452
  fireEvent.mouseLeave(screen.getByText('Fields'));
453
453
 
454
- await waitFor(() => expect(screen.queryByText('Field 1')).not.toBeInTheDocument());
454
+ await act(async () => {
455
+ await new Promise((resolve) => setTimeout(resolve, 350));
456
+ });
457
+
458
+ expect(screen.getByText('Field 1')).toBeInTheDocument();
455
459
  });
456
460
 
457
461
  it('closes active searchable submenu after outside click', async () => {