@nocobase/flow-engine 2.1.29 → 2.1.30

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.
@@ -419,6 +419,8 @@ const SUBMENU_MOTION_DISABLED = {
419
419
  const dropdownPersistRegistry = /* @__PURE__ */ new Map();
420
420
  const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
421
421
  const engine = (0, import_provider.useFlowEngine)();
422
+ const { getPrefixCls } = import_react.default.useContext(import_antd.ConfigProvider.ConfigContext);
423
+ const triggerId = import_react.default.useId();
422
424
  const [menuVisible, setMenuVisible] = (0, import_react.useState)(false);
423
425
  const [openKeys, setOpenKeys] = (0, import_react.useState)(/* @__PURE__ */ new Set());
424
426
  const [rootItems, setRootItems] = (0, import_react.useState)([]);
@@ -426,6 +428,9 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
426
428
  const activeSearchKeyRef = (0, import_react.useRef)(null);
427
429
  const closeByOutsideClickRef = (0, import_react.useRef)(false);
428
430
  const skipPreserveActiveSearchRef = (0, import_react.useRef)(false);
431
+ const triggerOpenClassName = `nb-lazy-dropdown-trigger-${triggerId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
432
+ const defaultOpenClassName = `${getPrefixCls("dropdown", props.prefixCls)}-open`;
433
+ const mergedOpenClassName = [props.openClassName ?? defaultOpenClassName, triggerOpenClassName].filter(Boolean).join(" ");
429
434
  const dropdownMaxHeight = useNiceDropdownMaxHeight();
430
435
  const t = engine.translate.bind(engine);
431
436
  const { items: menuItems, keepDropdownOpen, persistKey, stateVersion, refreshKeys, ...dropdownMenuProps } = menu;
@@ -510,7 +515,9 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
510
515
  if (!menuVisible) return;
511
516
  const markOutsideClick = /* @__PURE__ */ __name((event) => {
512
517
  const target = event.target;
513
- const isOutside = !(target == null ? void 0 : target.closest(".ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup"));
518
+ const isInsidePopup = target == null ? void 0 : target.closest(".ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup");
519
+ const isInsideCurrentTrigger = target == null ? void 0 : target.closest(`.${triggerOpenClassName}`);
520
+ const isOutside = !isInsidePopup && !isInsideCurrentTrigger;
514
521
  closeByOutsideClickRef.current = isOutside;
515
522
  if (isOutside) {
516
523
  closeMenu();
@@ -522,7 +529,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
522
529
  document.removeEventListener("pointerdown", markOutsideClick, true);
523
530
  document.removeEventListener("mousedown", markOutsideClick, true);
524
531
  };
525
- }, [closeMenu, menuVisible]);
532
+ }, [closeMenu, menuVisible, triggerOpenClassName]);
526
533
  (0, import_react.useEffect)(() => {
527
534
  if (!persistKey) return;
528
535
  const until = dropdownPersistRegistry.get(persistKey) || 0;
@@ -741,6 +748,7 @@ const LazyDropdown = /* @__PURE__ */ __name(({ menu, ...props }) => {
741
748
  open: menuVisible,
742
749
  destroyPopupOnHide: true,
743
750
  mouseLeaveDelay: props.mouseLeaveDelay ?? MENU_CLOSE_DELAY,
751
+ openClassName: mergedOpenClassName,
744
752
  overlayClassName,
745
753
  placement: "bottomLeft",
746
754
  menu: {
@@ -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": "步骤配置",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.1.29",
3
+ "version": "2.1.30",
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.29",
12
- "@nocobase/shared": "2.1.29",
11
+ "@nocobase/sdk": "2.1.30",
12
+ "@nocobase/shared": "2.1.30",
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": "d9848c5df07cef806cb938bb14b3f4ef5f3d0129"
40
+ "gitHead": "c1a4bff0fd3c8b30e06fc6f50ad33e054b04061a"
41
41
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { css } from '@emotion/css';
11
- import { Dropdown, DropdownProps, Empty, Input, InputProps, Spin } from 'antd';
11
+ import { ConfigProvider, Dropdown, DropdownProps, Empty, Input, InputProps, Spin } from 'antd';
12
12
  import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import { useFlowEngine } from '../../provider';
14
14
 
@@ -564,6 +564,8 @@ const dropdownPersistRegistry: Map<string, number> = new Map();
564
564
 
565
565
  const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
566
566
  const engine = useFlowEngine();
567
+ const { getPrefixCls } = React.useContext(ConfigProvider.ConfigContext);
568
+ const triggerId = React.useId();
567
569
  const [menuVisible, setMenuVisible] = useState(false);
568
570
  const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
569
571
  const [rootItems, setRootItems] = useState<Item[]>([]);
@@ -571,6 +573,11 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
571
573
  const activeSearchKeyRef = useRef<string | null>(null);
572
574
  const closeByOutsideClickRef = useRef(false);
573
575
  const skipPreserveActiveSearchRef = useRef(false);
576
+ const triggerOpenClassName = `nb-lazy-dropdown-trigger-${triggerId.replace(/[^a-zA-Z0-9_-]/g, '')}`;
577
+ const defaultOpenClassName = `${getPrefixCls('dropdown', props.prefixCls)}-open`;
578
+ const mergedOpenClassName = [props.openClassName ?? defaultOpenClassName, triggerOpenClassName]
579
+ .filter(Boolean)
580
+ .join(' ');
574
581
  const dropdownMaxHeight = useNiceDropdownMaxHeight();
575
582
  const t = engine.translate.bind(engine);
576
583
 
@@ -673,7 +680,9 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
673
680
 
674
681
  const markOutsideClick = (event: MouseEvent | PointerEvent) => {
675
682
  const target = event.target as HTMLElement | null;
676
- const isOutside = !target?.closest('.ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup');
683
+ const isInsidePopup = target?.closest('.ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup');
684
+ const isInsideCurrentTrigger = target?.closest(`.${triggerOpenClassName}`);
685
+ const isOutside = !isInsidePopup && !isInsideCurrentTrigger;
677
686
  closeByOutsideClickRef.current = isOutside;
678
687
  if (isOutside) {
679
688
  closeMenu();
@@ -686,7 +695,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
686
695
  document.removeEventListener('pointerdown', markOutsideClick, true);
687
696
  document.removeEventListener('mousedown', markOutsideClick, true);
688
697
  };
689
- }, [closeMenu, menuVisible]);
698
+ }, [closeMenu, menuVisible, triggerOpenClassName]);
690
699
 
691
700
  // 在挂载时,若存在 persistKey 且仍在持久期内,则尝试恢复打开状态
692
701
  useEffect(() => {
@@ -965,6 +974,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
965
974
  open={menuVisible}
966
975
  destroyPopupOnHide
967
976
  mouseLeaveDelay={props.mouseLeaveDelay ?? MENU_CLOSE_DELAY}
977
+ openClassName={mergedOpenClassName}
968
978
  overlayClassName={overlayClassName}
969
979
  placement="bottomLeft"
970
980
  menu={{
@@ -503,6 +503,85 @@ describe('transformItems - searchable flags', () => {
503
503
  await waitFor(() => expect(screen.queryByText('Fields')).not.toBeInTheDocument());
504
504
  });
505
505
 
506
+ it('keeps the dropdown open when clicking its already-hovered trigger', async () => {
507
+ const engine = new FlowEngine();
508
+ await engine.flowSettings.forceEnable();
509
+ class Parent extends FlowModel {}
510
+ engine.registerModels({ Parent });
511
+ const parent = engine.createModel<FlowModel>({ use: 'Parent' });
512
+ const user = userEvent.setup();
513
+
514
+ render(
515
+ <FlowEngineProvider engine={engine}>
516
+ <ConfigProvider>
517
+ <App>
518
+ <AddSubModelButton
519
+ model={parent}
520
+ subModelKey="items"
521
+ items={[
522
+ {
523
+ key: 'child',
524
+ label: 'Child',
525
+ createModelOptions: { use: 'Parent' },
526
+ },
527
+ ]}
528
+ >
529
+ Open
530
+ </AddSubModelButton>
531
+ </App>
532
+ </ConfigProvider>
533
+ </FlowEngineProvider>,
534
+ );
535
+
536
+ const trigger = screen.getByText('Open');
537
+ await user.hover(trigger);
538
+ await waitFor(() => expect(screen.getByText('Child')).toBeInTheDocument());
539
+ expect(trigger).toHaveClass('ant-dropdown-open');
540
+
541
+ await user.click(trigger);
542
+
543
+ expect(screen.getByText('Child')).toBeInTheDocument();
544
+ });
545
+
546
+ it('closes the dropdown when clicking another dropdown trigger', async () => {
547
+ const engine = new FlowEngine();
548
+ await engine.flowSettings.forceEnable();
549
+ class Parent extends FlowModel {}
550
+ engine.registerModels({ Parent });
551
+ const parent = engine.createModel<FlowModel>({ use: 'Parent' });
552
+ const user = userEvent.setup();
553
+
554
+ render(
555
+ <FlowEngineProvider engine={engine}>
556
+ <ConfigProvider>
557
+ <App>
558
+ <AddSubModelButton
559
+ model={parent}
560
+ subModelKey="firstItems"
561
+ items={[{ key: 'first-child', label: 'First child', createModelOptions: { use: 'Parent' } }]}
562
+ >
563
+ Open first
564
+ </AddSubModelButton>
565
+ <AddSubModelButton
566
+ model={parent}
567
+ subModelKey="secondItems"
568
+ items={[{ key: 'second-child', label: 'Second child', createModelOptions: { use: 'Parent' } }]}
569
+ >
570
+ Open second
571
+ </AddSubModelButton>
572
+ </App>
573
+ </ConfigProvider>
574
+ </FlowEngineProvider>,
575
+ );
576
+
577
+ await user.hover(screen.getByText('Open first'));
578
+ await waitFor(() => expect(screen.getByText('First child')).toBeInTheDocument());
579
+
580
+ fireEvent.pointerDown(screen.getByText('Open second'));
581
+
582
+ await waitFor(() => expect(screen.queryByText('First child')).not.toBeInTheDocument());
583
+ });
584
+
506
585
  it('switches away from active searchable submenu and resets its input', async () => {
507
586
  const engine = new FlowEngine();
508
587
  await engine.flowSettings.forceEnable();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+
12
+ import { getFlowEngineTranslation } from '../index';
13
+
14
+ describe('flow engine locale', () => {
15
+ it('translates the default secondary confirmation text into Chinese', () => {
16
+ expect(getFlowEngineTranslation('Please Confirm', 'zh-CN')).toBe('请确认');
17
+ expect(getFlowEngineTranslation('Are you sure you want to perform the action?', 'zh-CN')).toBe(
18
+ '确定要执行此操作吗?',
19
+ );
20
+ });
21
+ });
@@ -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",
@@ -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": "步骤配置",