@nocobase/flow-engine 2.1.29 → 2.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/acl/Acl.d.ts +2 -1
- package/lib/acl/Acl.js +28 -0
- package/lib/components/subModel/LazyDropdown.js +10 -2
- package/lib/locale/en-US.json +2 -0
- package/lib/locale/index.d.ts +4 -0
- package/lib/locale/zh-CN.json +2 -0
- package/package.json +4 -4
- package/src/acl/Acl.tsx +36 -1
- package/src/acl/__tests__/Acl.test.tsx +70 -0
- package/src/components/subModel/LazyDropdown.tsx +13 -3
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +79 -0
- package/src/locale/__tests__/index.test.ts +21 -0
- package/src/locale/en-US.json +2 -0
- package/src/locale/zh-CN.json +2 -0
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
|
|
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) {
|
|
@@ -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
|
|
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: {
|
package/lib/locale/en-US.json
CHANGED
|
@@ -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",
|
package/lib/locale/index.d.ts
CHANGED
|
@@ -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;
|
package/lib/locale/zh-CN.json
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "2.1.31",
|
|
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.
|
|
12
|
-
"@nocobase/shared": "2.1.
|
|
11
|
+
"@nocobase/sdk": "2.1.31",
|
|
12
|
+
"@nocobase/shared": "2.1.31",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "b5d46b76adb72b4fa736dd0e6c89d8a0bc83bfe8"
|
|
41
41
|
}
|
package/src/acl/Acl.tsx
CHANGED
|
@@ -16,7 +16,7 @@ interface CheckOptions {
|
|
|
16
16
|
actionName: string;
|
|
17
17
|
fields?: string[];
|
|
18
18
|
recordPkValue?: string | number;
|
|
19
|
-
allowedActions
|
|
19
|
+
allowedActions?: Record<string, Array<string | number>>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class ACL {
|
|
@@ -149,6 +149,41 @@ export class ACL {
|
|
|
149
149
|
return allowed;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
can(options: CheckOptions): boolean {
|
|
153
|
+
const { allowAll } = this.data;
|
|
154
|
+
if (allowAll) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { actionName, allowedActions, recordPkValue } = options;
|
|
159
|
+
const hasRecordPkValue = recordPkValue !== undefined && recordPkValue !== null;
|
|
160
|
+
const recordPermission =
|
|
161
|
+
hasRecordPkValue && allowedActions ? this.verifyScope(actionName, recordPkValue, allowedActions) : null;
|
|
162
|
+
if (hasRecordPkValue && allowedActions && recordPermission !== true) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const params = this.parseAction(options);
|
|
167
|
+
if (!params) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (!_.isEmpty(params.filter) && recordPermission !== true) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (!options.fields?.length) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const allowedFields: string[] = []
|
|
178
|
+
.concat(params.whitelist || [])
|
|
179
|
+
.concat(params.fields || [])
|
|
180
|
+
.concat(params.appends || []);
|
|
181
|
+
if (!allowedFields.length) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return options.fields.every((field) => allowedFields.includes(field));
|
|
185
|
+
}
|
|
186
|
+
|
|
152
187
|
async aclCheck(options: CheckOptions): Promise<boolean> {
|
|
153
188
|
// await this.load();
|
|
154
189
|
const { allowAll } = this.data;
|
|
@@ -71,6 +71,76 @@ describe('ACL', () => {
|
|
|
71
71
|
expect(notOk).toBe(false);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it('checks record update scope before field permission', () => {
|
|
75
|
+
const payload = {
|
|
76
|
+
data: {
|
|
77
|
+
allowAll: false,
|
|
78
|
+
actionAlias: {},
|
|
79
|
+
resources: ['posts'],
|
|
80
|
+
actions: { 'posts:update': { whitelist: ['title'] } },
|
|
81
|
+
strategy: { actions: [] },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const engine = makeEngine(payload);
|
|
85
|
+
const acl = new ACL(engine);
|
|
86
|
+
acl.setData(payload.data);
|
|
87
|
+
|
|
88
|
+
const options = {
|
|
89
|
+
dataSourceKey: 'main',
|
|
90
|
+
resourceName: 'posts',
|
|
91
|
+
actionName: 'update',
|
|
92
|
+
allowedActions: {
|
|
93
|
+
update: [1],
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
expect(acl.can({ ...options, recordPkValue: 1, fields: ['title'] })).toBe(true);
|
|
98
|
+
expect(acl.can({ ...options, recordPkValue: 2, fields: ['title'] })).toBe(false);
|
|
99
|
+
expect(acl.can({ ...options, recordPkValue: 1, fields: ['body'] })).toBe(false);
|
|
100
|
+
expect(acl.can({ ...options, recordPkValue: 0, fields: ['title'] })).toBe(false);
|
|
101
|
+
expect(
|
|
102
|
+
acl.can({
|
|
103
|
+
dataSourceKey: 'main',
|
|
104
|
+
resourceName: 'posts',
|
|
105
|
+
actionName: 'update',
|
|
106
|
+
fields: ['title'],
|
|
107
|
+
}),
|
|
108
|
+
).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('allows every field when a scoped action has no field restriction', () => {
|
|
112
|
+
const payload = {
|
|
113
|
+
data: {
|
|
114
|
+
allowAll: false,
|
|
115
|
+
actionAlias: {},
|
|
116
|
+
resources: ['posts'],
|
|
117
|
+
actions: {
|
|
118
|
+
'posts:update': {
|
|
119
|
+
filter: { createdById: '{{ ctx.state.currentUser.id }}' },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
strategy: { actions: [] },
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const engine = makeEngine(payload);
|
|
126
|
+
const acl = new ACL(engine);
|
|
127
|
+
acl.setData(payload.data);
|
|
128
|
+
|
|
129
|
+
const options = {
|
|
130
|
+
dataSourceKey: 'main',
|
|
131
|
+
resourceName: 'posts',
|
|
132
|
+
actionName: 'update',
|
|
133
|
+
allowedActions: {
|
|
134
|
+
update: [1],
|
|
135
|
+
},
|
|
136
|
+
fields: ['title'],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
expect(acl.can({ ...options, recordPkValue: 1 })).toBe(true);
|
|
140
|
+
expect(acl.can({ ...options, recordPkValue: 2 })).toBe(false);
|
|
141
|
+
expect(acl.can({ ...options, allowedActions: undefined, recordPkValue: undefined })).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
74
144
|
it('reloads permissions when auth token changes', async () => {
|
|
75
145
|
const payload1 = {
|
|
76
146
|
data: {
|
|
@@ -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
|
|
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
|
+
});
|
package/src/locale/en-US.json
CHANGED
|
@@ -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",
|
package/src/locale/zh-CN.json
CHANGED
|
@@ -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": "步骤配置",
|