@nocobase/flow-engine 2.1.0-beta.10 → 2.1.0-beta.12
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/components/FlowModelRenderer.d.ts +1 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +48 -9
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +19 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +332 -296
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +272 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +247 -0
- package/lib/components/subModel/AddSubModelButton.js +11 -0
- package/lib/flowContext.js +27 -0
- package/lib/runjs-context/setup.js +1 -0
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/package.json +5 -4
- package/src/__tests__/flowContext.test.ts +65 -1
- package/src/__tests__/runjsContext.test.ts +3 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/components/FlowModelRenderer.tsx +3 -1
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +17 -7
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +63 -9
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +457 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +95 -0
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +547 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +358 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +281 -0
- package/src/components/subModel/AddSubModelButton.tsx +15 -1
- package/src/flowContext.ts +30 -0
- package/src/runjs-context/setup.ts +1 -0
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
|
@@ -81,6 +81,7 @@ const snippets = {
|
|
|
81
81
|
"scene/detail/status-tag": /* @__PURE__ */ __name(() => import("./scene/detail/status-tag.snippet"), "scene/detail/status-tag"),
|
|
82
82
|
"scene/detail/relative-time": /* @__PURE__ */ __name(() => import("./scene/detail/relative-time.snippet"), "scene/detail/relative-time"),
|
|
83
83
|
"scene/detail/percentage-bar": /* @__PURE__ */ __name(() => import("./scene/detail/percentage-bar.snippet"), "scene/detail/percentage-bar"),
|
|
84
|
+
"scene/detail/set-field-style": /* @__PURE__ */ __name(() => import("./scene/detail/set-field-style.snippet"), "scene/detail/set-field-style"),
|
|
84
85
|
// scene/form
|
|
85
86
|
"scene/form/render-basic": /* @__PURE__ */ __name(() => import("./scene/form/render-basic.snippet"), "scene/form/render-basic"),
|
|
86
87
|
"scene/form/set-field-value": /* @__PURE__ */ __name(() => import("./scene/form/set-field-value.snippet"), "scene/form/set-field-value"),
|
|
@@ -98,7 +99,8 @@ const snippets = {
|
|
|
98
99
|
"scene/table/collection-selected-count": /* @__PURE__ */ __name(() => import("./scene/table/collection-selected-count.snippet"), "scene/table/collection-selected-count"),
|
|
99
100
|
"scene/table/iterate-selected-rows": /* @__PURE__ */ __name(() => import("./scene/table/iterate-selected-rows.snippet"), "scene/table/iterate-selected-rows"),
|
|
100
101
|
"scene/table/destroy-selected": /* @__PURE__ */ __name(() => import("./scene/table/destroy-selected.snippet"), "scene/table/destroy-selected"),
|
|
101
|
-
"scene/table/export-selected-json": /* @__PURE__ */ __name(() => import("./scene/table/export-selected-json.snippet"), "scene/table/export-selected-json")
|
|
102
|
+
"scene/table/export-selected-json": /* @__PURE__ */ __name(() => import("./scene/table/export-selected-json.snippet"), "scene/table/export-selected-json"),
|
|
103
|
+
"scene/table/set-cell-style": /* @__PURE__ */ __name(() => import("./scene/table/set-cell-style.snippet"), "scene/table/set-cell-style")
|
|
102
104
|
};
|
|
103
105
|
var snippets_default = snippets;
|
|
104
106
|
function registerRunJSSnippet(ref, loader, options) {
|
|
@@ -131,10 +133,19 @@ function normalizeScenes(def, key) {
|
|
|
131
133
|
return [];
|
|
132
134
|
}
|
|
133
135
|
__name(normalizeScenes, "normalizeScenes");
|
|
136
|
+
function normalizeSceneGroup(scene) {
|
|
137
|
+
const mapping = {
|
|
138
|
+
detailFieldEvent: "detail",
|
|
139
|
+
tableFieldEvent: "table",
|
|
140
|
+
formFieldEvent: "form"
|
|
141
|
+
};
|
|
142
|
+
return mapping[scene] || scene;
|
|
143
|
+
}
|
|
144
|
+
__name(normalizeSceneGroup, "normalizeSceneGroup");
|
|
134
145
|
function computeGroups(def, key) {
|
|
135
146
|
const scenes = normalizeScenes(def, key);
|
|
136
147
|
if (scenes.length) {
|
|
137
|
-
return scenes.map((scene) => `scene/${scene}`);
|
|
148
|
+
return Array.from(new Set(scenes.map((scene) => `scene/${normalizeSceneGroup(scene)}`)));
|
|
138
149
|
}
|
|
139
150
|
const parts = key.split("/");
|
|
140
151
|
if (!parts.length) return [];
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
import type { SnippetModule } from '../../types';
|
|
10
|
+
declare const snippet: SnippetModule;
|
|
11
|
+
export default snippet;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var set_field_style_snippet_exports = {};
|
|
28
|
+
__export(set_field_style_snippet_exports, {
|
|
29
|
+
default: () => set_field_style_snippet_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(set_field_style_snippet_exports);
|
|
32
|
+
const snippet = {
|
|
33
|
+
contexts: ["*"],
|
|
34
|
+
scenes: ["detailFieldEvent", "formFieldEvent"],
|
|
35
|
+
prefix: "sn-item-style",
|
|
36
|
+
label: "Set form item/details item style",
|
|
37
|
+
description: "Customize form item and details item container styles",
|
|
38
|
+
locales: {
|
|
39
|
+
"zh-CN": {
|
|
40
|
+
label: "\u8BBE\u7F6E\u8868\u5355\u9879/\u8BE6\u60C5\u9879\u6837\u5F0F",
|
|
41
|
+
description: "\u81EA\u5B9A\u4E49\u8868\u5355\u9879\u548C\u8BE6\u60C5\u9879\u5BB9\u5668\u6837\u5F0F"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
content: `
|
|
45
|
+
ctx.model.props.style = {
|
|
46
|
+
background: 'red',
|
|
47
|
+
};
|
|
48
|
+
`
|
|
49
|
+
};
|
|
50
|
+
var set_field_style_snippet_default = snippet;
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
import type { SnippetModule } from '../../types';
|
|
10
|
+
declare const snippet: SnippetModule;
|
|
11
|
+
export default snippet;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var set_cell_style_snippet_exports = {};
|
|
28
|
+
__export(set_cell_style_snippet_exports, {
|
|
29
|
+
default: () => set_cell_style_snippet_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(set_cell_style_snippet_exports);
|
|
32
|
+
const snippet = {
|
|
33
|
+
contexts: ["*"],
|
|
34
|
+
scenes: ["tableFieldEvent"],
|
|
35
|
+
prefix: "sn-table-cell-style",
|
|
36
|
+
label: "Set table cell style",
|
|
37
|
+
description: "Customize table field cell styles with onCell",
|
|
38
|
+
locales: {
|
|
39
|
+
"zh-CN": {
|
|
40
|
+
label: "\u8868\u683C\u5B57\u6BB5\u6837\u5F0F\u8BBE\u7F6E",
|
|
41
|
+
description: "\u901A\u8FC7 onCell \u81EA\u5B9A\u4E49\u8868\u683C\u5B57\u6BB5\u5355\u5143\u683C\u6837\u5F0F"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
content: `
|
|
45
|
+
ctx.model.props.onCell = (record, rowIndex) => {
|
|
46
|
+
return {
|
|
47
|
+
style: {
|
|
48
|
+
background: 'red',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
`
|
|
53
|
+
};
|
|
54
|
+
var set_cell_style_snippet_default = snippet;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.12",
|
|
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,9 +8,10 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.1.0-beta.12",
|
|
12
|
+
"@nocobase/shared": "2.1.0-beta.12",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
|
+
"axios": "^1.7.0",
|
|
14
15
|
"dayjs": "^1.11.9",
|
|
15
16
|
"dompurify": "^3.0.2",
|
|
16
17
|
"lodash": "^4.x",
|
|
@@ -36,5 +37,5 @@
|
|
|
36
37
|
],
|
|
37
38
|
"author": "NocoBase Team",
|
|
38
39
|
"license": "Apache-2.0",
|
|
39
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "25cee9643f42f850afc4adc33c55a56850ac730d"
|
|
40
41
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
11
12
|
import { FlowContext, FlowRuntimeContext, FlowRunJSContext, type PropertyMetaFactory } from '../flowContext';
|
|
12
13
|
import { FlowEngine } from '../flowEngine';
|
|
13
14
|
import { FlowModel } from '../models/flowModel';
|
|
@@ -1630,6 +1631,69 @@ describe('runAction delegation from runtime context', () => {
|
|
|
1630
1631
|
});
|
|
1631
1632
|
});
|
|
1632
1633
|
|
|
1634
|
+
describe('FlowContext request defaults', () => {
|
|
1635
|
+
class RequestModel extends FlowModel {}
|
|
1636
|
+
|
|
1637
|
+
afterEach(() => {
|
|
1638
|
+
vi.restoreAllMocks();
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
const createRequestContext = () => {
|
|
1642
|
+
const engine = new FlowEngine();
|
|
1643
|
+
engine.registerModels({ RequestModel });
|
|
1644
|
+
|
|
1645
|
+
const apiRequest = vi.fn(async (options) => options);
|
|
1646
|
+
const app = {
|
|
1647
|
+
getApiUrl(pathname = '') {
|
|
1648
|
+
return 'https://app.example.com/api/'.replace(/\/$/g, '') + '/' + pathname.replace(/^\//g, '');
|
|
1649
|
+
},
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
engine.context.defineProperty('api', { value: { request: apiRequest } as any });
|
|
1653
|
+
engine.context.defineProperty('app', { value: app });
|
|
1654
|
+
|
|
1655
|
+
const model = engine.createModel({ use: 'RequestModel' });
|
|
1656
|
+
const ctx = new FlowRuntimeContext(model, 'flow');
|
|
1657
|
+
const directAxiosRequest = vi.spyOn(axios, 'request').mockResolvedValue({ data: {} } as any);
|
|
1658
|
+
|
|
1659
|
+
return { ctx, apiRequest, directAxiosRequest };
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
it.each([
|
|
1663
|
+
['apiClient', 'users:list', 'api'],
|
|
1664
|
+
['apiClient', '/api/users:list', 'api'],
|
|
1665
|
+
['apiClient', 'https://app.example.com/api/users:list', 'api'],
|
|
1666
|
+
['direct axios', 'https://app.example.com/custom-api/users', 'axios'],
|
|
1667
|
+
])('should use %s for %s', async (_target, url, expected) => {
|
|
1668
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1669
|
+
|
|
1670
|
+
await ctx.request({ url, method: 'get' });
|
|
1671
|
+
|
|
1672
|
+
if (expected === 'api') {
|
|
1673
|
+
expect(apiRequest).toHaveBeenCalledTimes(1);
|
|
1674
|
+
expect(directAxiosRequest).not.toHaveBeenCalled();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1679
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it('should use direct axios for cross-origin absolute urls', async () => {
|
|
1683
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1684
|
+
|
|
1685
|
+
await ctx.request({ url: 'https://api.example.com/users', method: 'get', skipAuth: false });
|
|
1686
|
+
|
|
1687
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1688
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1689
|
+
expect(directAxiosRequest.mock.calls[0][0]).toMatchObject({
|
|
1690
|
+
url: 'https://api.example.com/users',
|
|
1691
|
+
method: 'get',
|
|
1692
|
+
skipAuth: false,
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1633
1697
|
describe('FlowContext delayed meta loading', () => {
|
|
1634
1698
|
// 测试场景:属性定义时 meta 为异步函数,首次访问时延迟加载
|
|
1635
1699
|
// 输入:属性带有异步 meta 函数
|
|
@@ -38,6 +38,7 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
38
38
|
'JSBlockModel',
|
|
39
39
|
'JSFieldModel',
|
|
40
40
|
'JSItemModel',
|
|
41
|
+
'JSItemActionModel',
|
|
41
42
|
'JSColumnModel',
|
|
42
43
|
'FormJSFieldItemModel',
|
|
43
44
|
'JSRecordActionModel',
|
|
@@ -58,8 +59,10 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
58
59
|
it('should expose scene metadata for contexts', () => {
|
|
59
60
|
expect(getRunJSScenesForModel('JSBlockModel', 'v1')).toEqual(['block']);
|
|
60
61
|
expect(getRunJSScenesForModel('JSFieldModel', 'v1')).toEqual(['detail']);
|
|
62
|
+
expect(getRunJSScenesForModel('JSItemActionModel', 'v1')).toEqual(['table']);
|
|
61
63
|
expect(getRunJSScenesForModel('JSBlockModel', 'v2')).toEqual(['block']);
|
|
62
64
|
expect(getRunJSScenesForModel('JSFieldModel', 'v2')).toEqual(['detail']);
|
|
65
|
+
expect(getRunJSScenesForModel('JSItemActionModel', 'v2')).toEqual(['table']);
|
|
63
66
|
expect(getRunJSScenesForModel('UnknownModel', 'v1')).toEqual([]);
|
|
64
67
|
expect(getRunJSScenesForModel('UnknownModel', 'v2')).toEqual([]);
|
|
65
68
|
});
|
|
@@ -186,6 +186,7 @@ describe('RunJS Context Runtime Behavior', () => {
|
|
|
186
186
|
'JSBlockModel',
|
|
187
187
|
'JSFieldModel',
|
|
188
188
|
'JSItemModel',
|
|
189
|
+
'JSItemActionModel',
|
|
189
190
|
'JSColumnModel',
|
|
190
191
|
'FormJSFieldItemModel',
|
|
191
192
|
'JSRecordActionModel',
|
|
@@ -237,6 +238,7 @@ describe('RunJS Context Runtime Behavior', () => {
|
|
|
237
238
|
'JSBlockModel',
|
|
238
239
|
'JSFieldModel',
|
|
239
240
|
'JSItemModel',
|
|
241
|
+
'JSItemActionModel',
|
|
240
242
|
'JSColumnModel',
|
|
241
243
|
'FormJSFieldItemModel',
|
|
242
244
|
'JSRecordActionModel',
|
|
@@ -112,6 +112,27 @@ describe('RunJS Snippets', () => {
|
|
|
112
112
|
expect(multiScene?.scenes).toEqual(expect.arrayContaining(['detail', 'table']));
|
|
113
113
|
expect(multiScene?.groups).toEqual(expect.arrayContaining(['scene/detail', 'scene/table']));
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
it('should expose new style snippets for matching contexts', async () => {
|
|
117
|
+
const tableSnippets = await listSnippetsForContext('JSColumnRunJSContext', 'v1', 'zh-CN');
|
|
118
|
+
const fieldSnippets = await listSnippetsForContext('FormJSFieldItemRunJSContext', 'v1', 'zh-CN');
|
|
119
|
+
const detailEventSnippets = await listSnippetsForContext('DetailsItemModel', 'v1', 'zh-CN');
|
|
120
|
+
const tableEventSnippets = await listSnippetsForContext('TableColumnModel', 'v1', 'zh-CN');
|
|
121
|
+
|
|
122
|
+
const tableStyle = tableSnippets.find((s) => s.ref === 'scene/table/set-cell-style');
|
|
123
|
+
expect(tableStyle?.name).toBe('表格字段样式设置');
|
|
124
|
+
expect(tableStyle?.body).toContain('ctx.model.props.onCell');
|
|
125
|
+
expect(tableStyle?.scenes).toEqual(['tableFieldEvent']);
|
|
126
|
+
|
|
127
|
+
const fieldStyle = fieldSnippets.find((s) => s.ref === 'scene/detail/set-field-style');
|
|
128
|
+
expect(fieldStyle?.name).toBe('设置表单项/详情项样式');
|
|
129
|
+
expect(fieldStyle?.body).toContain('ctx.model.props.style');
|
|
130
|
+
expect(fieldStyle?.scenes).toEqual(expect.arrayContaining(['detailFieldEvent', 'formFieldEvent']));
|
|
131
|
+
expect(fieldStyle?.groups).toEqual(expect.arrayContaining(['scene/detail', 'scene/form']));
|
|
132
|
+
|
|
133
|
+
expect(detailEventSnippets.some((s) => s.ref === 'scene/detail/set-field-style')).toBe(true);
|
|
134
|
+
expect(tableEventSnippets.some((s) => s.ref === 'scene/table/set-cell-style')).toBe(true);
|
|
135
|
+
});
|
|
115
136
|
});
|
|
116
137
|
|
|
117
138
|
describe('New snippets', () => {
|
|
@@ -69,7 +69,7 @@ export interface FlowModelRendererProps {
|
|
|
69
69
|
showBackground?: boolean;
|
|
70
70
|
showBorder?: boolean;
|
|
71
71
|
showDragHandle?: boolean;
|
|
72
|
-
/**
|
|
72
|
+
/** 自定义工具栏样式,`top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */
|
|
73
73
|
style?: React.CSSProperties;
|
|
74
74
|
/**
|
|
75
75
|
* @default 'inside'
|
|
@@ -112,6 +112,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
|
|
112
112
|
showBackground?: boolean;
|
|
113
113
|
showBorder?: boolean;
|
|
114
114
|
showDragHandle?: boolean;
|
|
115
|
+
/** `top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */
|
|
115
116
|
style?: React.CSSProperties;
|
|
116
117
|
/**
|
|
117
118
|
* @default 'inside'
|
|
@@ -182,6 +183,7 @@ const FlowModelRendererCore: React.FC<{
|
|
|
182
183
|
showBackground?: boolean;
|
|
183
184
|
showBorder?: boolean;
|
|
184
185
|
showDragHandle?: boolean;
|
|
186
|
+
/** `top/left/right/bottom` 会作为 portal overlay 的 inset 使用 */
|
|
185
187
|
style?: React.CSSProperties;
|
|
186
188
|
/**
|
|
187
189
|
* @default 'inside'
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
-
import { render, cleanup, waitFor } from '@testing-library/react';
|
|
12
|
+
import { render, cleanup, fireEvent, waitFor } from '@testing-library/react';
|
|
13
13
|
import { App, ConfigProvider } from 'antd';
|
|
14
14
|
import { FlowEngine } from '../../flowEngine';
|
|
15
15
|
import { FlowModel, ModelRenderMode } from '../../models/flowModel';
|
|
@@ -94,6 +94,16 @@ const clickDeleteFromLastDropdown = async () => {
|
|
|
94
94
|
menu.onClick?.({ key: 'delete' });
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
const getHost = (element: HTMLElement) => element.closest('[data-has-float-menu="true"]') as HTMLDivElement;
|
|
98
|
+
|
|
99
|
+
const hoverHostAndClickDelete = async (element: HTMLElement) => {
|
|
100
|
+
const host = getHost(element);
|
|
101
|
+
if (host) {
|
|
102
|
+
fireEvent.mouseEnter(host);
|
|
103
|
+
}
|
|
104
|
+
await clickDeleteFromLastDropdown();
|
|
105
|
+
};
|
|
106
|
+
|
|
97
107
|
// ---------------- Tests ----------------
|
|
98
108
|
describe('Delete problematic model via FlowSettings menu', () => {
|
|
99
109
|
beforeEach(() => {
|
|
@@ -120,7 +130,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
120
130
|
// satisfy FlowsFloatContextMenu styles
|
|
121
131
|
model.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
122
132
|
|
|
123
|
-
render(
|
|
133
|
+
const { findByTestId } = render(
|
|
124
134
|
<ConfigProvider>
|
|
125
135
|
<App>
|
|
126
136
|
<FlowEngineProvider engine={engine}>
|
|
@@ -130,7 +140,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
130
140
|
</ConfigProvider>,
|
|
131
141
|
);
|
|
132
142
|
|
|
133
|
-
await
|
|
143
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
134
144
|
expect(engine.getModel(model.uid)).toBeUndefined();
|
|
135
145
|
});
|
|
136
146
|
|
|
@@ -163,7 +173,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
163
173
|
parent.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
164
174
|
child.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
165
175
|
|
|
166
|
-
render(
|
|
176
|
+
const { findByTestId } = render(
|
|
167
177
|
<ConfigProvider>
|
|
168
178
|
<App>
|
|
169
179
|
<FlowEngineProvider engine={engine}>
|
|
@@ -173,7 +183,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
173
183
|
</ConfigProvider>,
|
|
174
184
|
);
|
|
175
185
|
|
|
176
|
-
await
|
|
186
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
177
187
|
expect(engine.getModel(child.uid)).toBeUndefined();
|
|
178
188
|
const remain = (parent.subModels as any).items || [];
|
|
179
189
|
expect(remain.find((m: FlowModel) => m.uid === child.uid)).toBeUndefined();
|
|
@@ -208,7 +218,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
208
218
|
parent.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
209
219
|
child.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
|
|
210
220
|
|
|
211
|
-
render(
|
|
221
|
+
const { findByTestId } = render(
|
|
212
222
|
<ConfigProvider>
|
|
213
223
|
<App>
|
|
214
224
|
<FlowEngineProvider engine={engine}>
|
|
@@ -218,7 +228,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
|
|
|
218
228
|
</ConfigProvider>,
|
|
219
229
|
);
|
|
220
230
|
|
|
221
|
-
await
|
|
231
|
+
await hoverHostAndClickDelete(await findByTestId('result'));
|
|
222
232
|
expect(engine.getModel(child.uid)).toBeUndefined();
|
|
223
233
|
const remain = (parent.subModels as any).cells || [];
|
|
224
234
|
expect(remain.find((m: FlowModel) => m.uid === child.uid)).toBeUndefined();
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ExclamationCircleOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
|
11
|
+
import { css } from '@emotion/css';
|
|
11
12
|
import type { DropdownProps, MenuProps } from 'antd';
|
|
12
13
|
import { App, Dropdown, Modal, Tooltip, theme } from 'antd';
|
|
13
14
|
import React, { startTransition, useCallback, useEffect, useMemo, useState, FC } from 'react';
|
|
@@ -188,15 +189,42 @@ interface DefaultSettingsIconProps {
|
|
|
188
189
|
showCopyUidButton?: boolean;
|
|
189
190
|
menuLevels?: number; // Menu levels: 1=current model only (default), 2=include sub-models
|
|
190
191
|
flattenSubMenus?: boolean; // Whether to flatten sub-menus: false=group by model (default), true=flatten all
|
|
192
|
+
onDropdownVisibleChange?: (open: boolean) => void;
|
|
193
|
+
getPopupContainer?: DropdownProps['getPopupContainer'];
|
|
191
194
|
[key: string]: any; // Allow additional props
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
const TOOLBAR_ICONS_SELECTOR = '.nb-toolbar-container-icons';
|
|
198
|
+
const TOOLBAR_CONTAINER_SELECTOR = '.nb-toolbar-container';
|
|
199
|
+
const TOOLBAR_DROPDOWN_OVERLAY_CLASS = css`
|
|
200
|
+
width: max-content;
|
|
201
|
+
min-width: max-content;
|
|
202
|
+
|
|
203
|
+
.ant-dropdown-menu {
|
|
204
|
+
width: max-content;
|
|
205
|
+
min-width: max-content;
|
|
206
|
+
}
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
const getToolbarPopupContainer = (triggerNode?: HTMLElement | null) => {
|
|
210
|
+
if (!triggerNode) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
(triggerNode.closest(TOOLBAR_ICONS_SELECTOR) as HTMLElement | null) ||
|
|
216
|
+
(triggerNode.closest(TOOLBAR_CONTAINER_SELECTOR) as HTMLElement | null)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
194
220
|
export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
195
221
|
model,
|
|
196
222
|
showDeleteButton = true,
|
|
197
223
|
showCopyUidButton = true,
|
|
198
224
|
menuLevels = 1, // 默认一级菜单
|
|
199
225
|
flattenSubMenus = true,
|
|
226
|
+
onDropdownVisibleChange,
|
|
227
|
+
getPopupContainer,
|
|
200
228
|
}) => {
|
|
201
229
|
const { message } = App.useApp();
|
|
202
230
|
const t = useMemo(() => getT(model), [model]);
|
|
@@ -210,15 +238,38 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
210
238
|
const [isLoading, setIsLoading] = useState(true);
|
|
211
239
|
const closeDropdown = useCallback(() => {
|
|
212
240
|
setVisible(false);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
onDropdownVisibleChange?.(false);
|
|
242
|
+
}, [onDropdownVisibleChange]);
|
|
243
|
+
const resolvePopupContainer = useCallback<NonNullable<DropdownProps['getPopupContainer']>>(
|
|
244
|
+
(triggerNode) => {
|
|
245
|
+
// 工具栏自身容器必须优先,保证鼠标从 icon 移到菜单时仍处于同一 hover 树。
|
|
246
|
+
// 弹窗场景的裁剪问题由 useFloatToolbarPortal 负责把 toolbar 挂到正确的 popup host。
|
|
247
|
+
return (
|
|
248
|
+
getToolbarPopupContainer(triggerNode) ||
|
|
249
|
+
getPopupContainer?.(triggerNode) ||
|
|
250
|
+
triggerNode?.parentElement ||
|
|
251
|
+
document.body
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
[getPopupContainer],
|
|
255
|
+
);
|
|
256
|
+
const handleOpenChange: DropdownProps['onOpenChange'] = useCallback(
|
|
257
|
+
(nextOpen: boolean, info) => {
|
|
258
|
+
if (info.source === 'trigger' || nextOpen) {
|
|
259
|
+
// 当鼠标快速滑过时,终止菜单的渲染,防止卡顿
|
|
260
|
+
startTransition(() => {
|
|
261
|
+
setVisible(nextOpen);
|
|
262
|
+
});
|
|
263
|
+
onDropdownVisibleChange?.(nextOpen);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[onDropdownVisibleChange],
|
|
267
|
+
);
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
return () => {
|
|
270
|
+
onDropdownVisibleChange?.(false);
|
|
271
|
+
};
|
|
272
|
+
}, [onDropdownVisibleChange]);
|
|
222
273
|
const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]);
|
|
223
274
|
useEffect(() => {
|
|
224
275
|
let mounted = true;
|
|
@@ -833,6 +884,9 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
833
884
|
|
|
834
885
|
return (
|
|
835
886
|
<Dropdown
|
|
887
|
+
getPopupContainer={resolvePopupContainer}
|
|
888
|
+
overlayClassName={TOOLBAR_DROPDOWN_OVERLAY_CLASS}
|
|
889
|
+
overlayStyle={{ width: 'max-content', minWidth: 'max-content' }}
|
|
836
890
|
onOpenChange={handleOpenChange}
|
|
837
891
|
open={visible}
|
|
838
892
|
menu={{
|