@nocobase/flow-engine 2.1.10 → 2.1.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/dnd/index.js +9 -2
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
- package/lib/flowContext.d.ts +1 -1
- package/lib/flowContext.js +32 -8
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/resources/apiResource.js +2 -1
- package/lib/resources/baseRecordResource.js +6 -17
- package/lib/resources/multiRecordResource.js +13 -3
- package/lib/resources/singleRecordResource.js +7 -2
- package/lib/utils/dataSourceDirty.d.ts +20 -0
- package/lib/utils/dataSourceDirty.js +139 -0
- package/lib/utils/dirtyAwareApiClient.d.ts +11 -0
- package/lib/utils/dirtyAwareApiClient.js +378 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +11 -0
- package/lib/utils/openViewRouteState.d.ts +28 -0
- package/lib/utils/openViewRouteState.js +125 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +3 -0
- package/lib/utils/parsePathnameToViewParams.js +18 -1
- package/lib/views/ViewNavigation.js +5 -0
- package/package.json +4 -4
- package/src/__tests__/flowContext.test.ts +131 -0
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
- package/src/components/dnd/index.tsx +11 -2
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
- package/src/flowContext.ts +45 -8
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/resources/apiResource.ts +2 -1
- package/src/resources/baseRecordResource.ts +6 -23
- package/src/resources/multiRecordResource.ts +13 -3
- package/src/resources/singleRecordResource.ts +6 -1
- package/src/utils/__tests__/dirtyAwareApiClient.test.ts +392 -0
- package/src/utils/__tests__/openViewRouteState.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +36 -0
- package/src/utils/dataSourceDirty.ts +126 -0
- package/src/utils/dirtyAwareApiClient.ts +430 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/openViewRouteState.ts +107 -0
- package/src/utils/parsePathnameToViewParams.ts +23 -1
- package/src/views/ViewNavigation.ts +6 -1
- package/src/views/__tests__/ViewNavigation.test.ts +15 -0
|
@@ -32,6 +32,7 @@ __export(ViewNavigation_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(ViewNavigation_exports);
|
|
34
34
|
var import_reactive = require("../reactive");
|
|
35
|
+
var import_utils = require("../utils");
|
|
35
36
|
function encodeFilterByTk(val) {
|
|
36
37
|
if (val === void 0 || val === null) return "";
|
|
37
38
|
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
@@ -63,6 +64,10 @@ function generatePathnameFromViewParams(viewParams, options = {}) {
|
|
|
63
64
|
segments.push("view");
|
|
64
65
|
}
|
|
65
66
|
segments.push(viewParam.viewUid);
|
|
67
|
+
const openViewRouteStateToken = (0, import_utils.encodeOpenViewRouteState)(viewParam.viewUid, viewParam.openViewRouteState);
|
|
68
|
+
if (openViewRouteStateToken) {
|
|
69
|
+
segments.push("opts", openViewRouteStateToken);
|
|
70
|
+
}
|
|
66
71
|
if (viewParam.tabUid) {
|
|
67
72
|
segments.push("tab", viewParam.tabUid);
|
|
68
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.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,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.12",
|
|
12
|
+
"@nocobase/shared": "2.1.12",
|
|
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": "3a69ed089fbb5a3e8700c0134f83d654f0190d25"
|
|
41
41
|
}
|
|
@@ -14,6 +14,8 @@ import { FlowEngine } from '../flowEngine';
|
|
|
14
14
|
import { FlowModel } from '../models/flowModel';
|
|
15
15
|
import { RunJSContextRegistry } from '../runjs-context/registry';
|
|
16
16
|
import { setupRunJSContexts } from '../runjs-context/setup';
|
|
17
|
+
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
18
|
+
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
17
19
|
|
|
18
20
|
describe('FlowContext properties and methods', () => {
|
|
19
21
|
it('should return static property value', () => {
|
|
@@ -177,6 +179,49 @@ describe('FlowContext properties and methods', () => {
|
|
|
177
179
|
});
|
|
178
180
|
});
|
|
179
181
|
|
|
182
|
+
it('should expose current role as a top-level variable', async () => {
|
|
183
|
+
const engine = new FlowEngine();
|
|
184
|
+
const ctx = engine.context;
|
|
185
|
+
ctx.defineProperty('api', { value: { auth: { role: 'admin' } } });
|
|
186
|
+
|
|
187
|
+
expect(ctx.role).toBe('admin');
|
|
188
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toBe('admin');
|
|
189
|
+
|
|
190
|
+
const roleNode = ctx.getPropertyMetaTree().find((node) => node.name === 'role');
|
|
191
|
+
expect(roleNode).toMatchObject({
|
|
192
|
+
name: 'role',
|
|
193
|
+
title: '{{t("Current role")}}',
|
|
194
|
+
paths: ['role'],
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should expose actual role names for union role mode', async () => {
|
|
199
|
+
const engine = new FlowEngine();
|
|
200
|
+
const ctx = engine.context;
|
|
201
|
+
ctx.defineProperty('api', { value: { auth: { role: '__union__' } } });
|
|
202
|
+
ctx.defineProperty('user', {
|
|
203
|
+
value: {
|
|
204
|
+
roles: [
|
|
205
|
+
{ name: 'admin', title: 'Admin' },
|
|
206
|
+
{ name: 'member', title: 'Member' },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(ctx.role).toEqual(['admin', 'member']);
|
|
212
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toEqual(['admin', 'member']);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should expose an empty role list for union role mode without user roles', async () => {
|
|
216
|
+
const engine = new FlowEngine();
|
|
217
|
+
const ctx = engine.context;
|
|
218
|
+
ctx.defineProperty('api', { value: { auth: { role: '__union__' } } });
|
|
219
|
+
ctx.defineProperty('user', { value: {} });
|
|
220
|
+
|
|
221
|
+
expect(ctx.role).toEqual([]);
|
|
222
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
|
|
180
225
|
it('should throw sync error in get', () => {
|
|
181
226
|
const ctx = new FlowContext();
|
|
182
227
|
ctx.defineProperty('error', {
|
|
@@ -1386,6 +1431,92 @@ describe('FlowEngine context', () => {
|
|
|
1386
1431
|
expect(engine.context.appName).toBe('NocoBase');
|
|
1387
1432
|
});
|
|
1388
1433
|
|
|
1434
|
+
it('ctx.api should return a dirty-aware wrapper for static api properties', async () => {
|
|
1435
|
+
const engine = new FlowEngine();
|
|
1436
|
+
const update = vi.fn(async () => ({ data: { data: { id: 1 } } }));
|
|
1437
|
+
const api = {
|
|
1438
|
+
auth: { locale: 'zh-CN' },
|
|
1439
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
1440
|
+
resource: vi.fn(() => ({ update })),
|
|
1441
|
+
};
|
|
1442
|
+
engine.context.defineProperty('api', { value: api });
|
|
1443
|
+
|
|
1444
|
+
await engine.context.api.resource('posts').update({ filterByTk: 1, values: { title: 't' } });
|
|
1445
|
+
|
|
1446
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
1447
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1448
|
+
expect(engine.context.api).toBe(engine.context.api);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
it('ctx.api should stay dirty-aware when resolved from a scoped context delegate', async () => {
|
|
1452
|
+
const root = new FlowEngine();
|
|
1453
|
+
const scoped = createViewScopedEngine(root);
|
|
1454
|
+
const update = vi.fn(async () => ({ data: { data: { id: 1 } } }));
|
|
1455
|
+
root.context.defineProperty('api', {
|
|
1456
|
+
value: {
|
|
1457
|
+
auth: { locale: 'zh-CN' },
|
|
1458
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
1459
|
+
resource: vi.fn(() => ({ update })),
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
await scoped.context.api.resource('posts').update({ filterByTk: 1, values: { title: 't' } });
|
|
1464
|
+
|
|
1465
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
1466
|
+
expect(root.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1467
|
+
expect(scoped.context.engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it('ctx.request should use the dirty-aware api wrapper', async () => {
|
|
1471
|
+
const engine = new FlowEngine();
|
|
1472
|
+
const request = vi.fn(async () => ({ data: { ok: true } }));
|
|
1473
|
+
engine.context.defineProperty('api', {
|
|
1474
|
+
value: {
|
|
1475
|
+
auth: { locale: 'zh-CN' },
|
|
1476
|
+
request,
|
|
1477
|
+
resource: vi.fn(),
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
await engine.context.request({
|
|
1482
|
+
resource: 'posts',
|
|
1483
|
+
action: 'update',
|
|
1484
|
+
headers: { 'X-Data-Source': 'analytics' },
|
|
1485
|
+
params: { filterByTk: 1 },
|
|
1486
|
+
} as any);
|
|
1487
|
+
|
|
1488
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
1489
|
+
expect(engine.getDataSourceDirtyVersion('analytics', 'posts')).toBe(1);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('ctx.request should use the caller context when resolved through a scoped delegate', async () => {
|
|
1493
|
+
const root = new FlowEngine();
|
|
1494
|
+
const scoped = createViewScopedEngine(root);
|
|
1495
|
+
const callerCtx = new FlowContext();
|
|
1496
|
+
const dirtyEvents: Array<{ dataSourceKey: string; resourceNames: string[] }> = [];
|
|
1497
|
+
const request = vi.fn(async () => ({ data: { ok: true } }));
|
|
1498
|
+
root.context.defineProperty('api', {
|
|
1499
|
+
value: {
|
|
1500
|
+
auth: { locale: 'zh-CN' },
|
|
1501
|
+
request,
|
|
1502
|
+
resource: vi.fn(),
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
callerCtx.addDelegate(scoped.context);
|
|
1506
|
+
scoped.context.engine.emitter.on(DATA_SOURCE_DIRTY_EVENT, (event) => dirtyEvents.push(event));
|
|
1507
|
+
|
|
1508
|
+
await callerCtx.request({
|
|
1509
|
+
resource: 'posts',
|
|
1510
|
+
action: 'update',
|
|
1511
|
+
params: { filterByTk: 1 },
|
|
1512
|
+
} as any);
|
|
1513
|
+
|
|
1514
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
1515
|
+
expect(root.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1516
|
+
expect(scoped.context.engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1517
|
+
expect(dirtyEvents).toEqual([{ dataSourceKey: 'main', resourceNames: ['posts'] }]);
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1389
1520
|
it('ctx.sql should resolve template variables from caller context in delegate chain', async () => {
|
|
1390
1521
|
const engine = new FlowEngine();
|
|
1391
1522
|
const request = vi.fn(async () => ({ data: { data: [] } }));
|
|
@@ -11,6 +11,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
11
11
|
import { FlowEngine } from '../flowEngine';
|
|
12
12
|
import { MultiRecordResource } from '../resources/multiRecordResource';
|
|
13
13
|
import { SingleRecordResource } from '../resources/singleRecordResource';
|
|
14
|
+
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
14
15
|
|
|
15
16
|
describe('FlowEngine dataSource dirty registry', () => {
|
|
16
17
|
it('tracks versions per dataSourceKey + resourceName', () => {
|
|
@@ -60,4 +61,54 @@ describe('FlowEngine dataSource dirty registry', () => {
|
|
|
60
61
|
// plus root collection (safety)
|
|
61
62
|
expect(markSpy).toHaveBeenCalledWith('main', 'users');
|
|
62
63
|
});
|
|
64
|
+
|
|
65
|
+
it('marks dirty once for record write helpers when using the dirty-aware context api', async () => {
|
|
66
|
+
const engine = new FlowEngine();
|
|
67
|
+
const request = vi.fn(async () => ({ data: { data: { id: 1 }, meta: {} } }));
|
|
68
|
+
const dirtyEvents: Array<{ dataSourceKey: string; resourceNames: string[] }> = [];
|
|
69
|
+
engine.context.defineProperty('api', {
|
|
70
|
+
value: {
|
|
71
|
+
auth: { locale: 'zh-CN' },
|
|
72
|
+
request,
|
|
73
|
+
resource: vi.fn(),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
engine.emitter.on(DATA_SOURCE_DIRTY_EVENT, (event) => dirtyEvents.push(event));
|
|
77
|
+
|
|
78
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
79
|
+
multi.setDataSourceKey('main').setResourceName('posts');
|
|
80
|
+
await multi.create({ title: 't' } as any, { refresh: false });
|
|
81
|
+
|
|
82
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
84
|
+
expect(dirtyEvents).toEqual([{ dataSourceKey: 'main', resourceNames: ['posts'] }]);
|
|
85
|
+
|
|
86
|
+
const single = engine.createResource(SingleRecordResource);
|
|
87
|
+
single.setDataSourceKey('main').setResourceName('posts').setFilterByTk(1);
|
|
88
|
+
await single.save({ title: 'u' } as any, { refresh: false });
|
|
89
|
+
|
|
90
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
91
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
|
|
92
|
+
expect(dirtyEvents).toEqual([
|
|
93
|
+
{ dataSourceKey: 'main', resourceNames: ['posts'] },
|
|
94
|
+
{ dataSourceKey: 'main', resourceNames: ['posts'] },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('still marks dirty for direct runAction writes', async () => {
|
|
99
|
+
const engine = new FlowEngine();
|
|
100
|
+
engine.context.defineProperty('api', {
|
|
101
|
+
value: {
|
|
102
|
+
auth: { locale: 'zh-CN' },
|
|
103
|
+
request: vi.fn(async () => ({ data: { data: { id: 1 }, meta: {} } })),
|
|
104
|
+
resource: vi.fn(),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
109
|
+
multi.setDataSourceKey('main').setResourceName('posts');
|
|
110
|
+
await multi.runAction('create', { data: { title: 't' } });
|
|
111
|
+
|
|
112
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
113
|
+
});
|
|
63
114
|
});
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
|
11
|
-
import { FlowEngineContext, FlowRunJSContext } from '../flowContext';
|
|
11
|
+
import { FlowContext, FlowEngineContext, FlowRunJSContext } from '../flowContext';
|
|
12
|
+
import { RUNJS_OPEN_VIEW_ROUTE_STATE } from '../utils/openViewRouteState';
|
|
12
13
|
import { FlowEngine } from '../flowEngine';
|
|
13
|
-
import { FlowContext } from '../flowContext';
|
|
14
14
|
import { setupRunJSContexts } from '../runjs-context/setup';
|
|
15
15
|
import { createJSRunnerWithVersion } from '..';
|
|
16
16
|
import { RunJSContextRegistry } from '../runjs-context/registry';
|
|
@@ -267,6 +267,19 @@ describe('RunJS Runtime Features', () => {
|
|
|
267
267
|
expect(runCtx.libs.dayjs).toBeDefined();
|
|
268
268
|
expect(runCtx.libs.antdIcons).toBeDefined();
|
|
269
269
|
});
|
|
270
|
+
|
|
271
|
+
it('should mark ctx.openView calls with route state only when RunJS passes display overrides', async () => {
|
|
272
|
+
const parentCtx = new FlowContext();
|
|
273
|
+
const openView = vi.fn(async () => undefined);
|
|
274
|
+
parentCtx.defineMethod('openView', openView);
|
|
275
|
+
|
|
276
|
+
const runCtx = new FlowRunJSContext(parentCtx);
|
|
277
|
+
await (runCtx as any).openView('popup', { mode: 'dialog', size: 'large' });
|
|
278
|
+
await (runCtx as any).openView('popup', { filterByTk: 1 });
|
|
279
|
+
|
|
280
|
+
expect(openView.mock.calls[0][1][RUNJS_OPEN_VIEW_ROUTE_STATE]).toEqual({ mode: 'dialog', size: 'large' });
|
|
281
|
+
expect(Object.prototype.hasOwnProperty.call(openView.mock.calls[1][1], RUNJS_OPEN_VIEW_ROUTE_STATE)).toBe(false);
|
|
282
|
+
});
|
|
270
283
|
});
|
|
271
284
|
|
|
272
285
|
describe('Actual code execution', () => {
|
|
@@ -35,6 +35,10 @@ type ToolbarDragAnchorDetail = {
|
|
|
35
35
|
point: ToolbarDragAnchorPoint | null;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
const getToolbarModelUidFromNode = (node: HTMLElement | null): string | null => {
|
|
39
|
+
return node?.closest<HTMLElement>('.nb-toolbar-container[data-model-uid]')?.getAttribute('data-model-uid') || null;
|
|
40
|
+
};
|
|
41
|
+
|
|
38
42
|
export const resolveOverlayAnchorTransform = ({
|
|
39
43
|
activeId,
|
|
40
44
|
active,
|
|
@@ -62,7 +66,7 @@ export const resolveOverlayAnchorTransform = ({
|
|
|
62
66
|
const resolveDraggableHostNode = (activatorNode: HTMLElement | null) => {
|
|
63
67
|
const ownerDocument = activatorNode?.ownerDocument;
|
|
64
68
|
const floatToolbarContainer = activatorNode?.closest<HTMLElement>('.nb-toolbar-container[data-model-uid]');
|
|
65
|
-
const toolbarModelUid =
|
|
69
|
+
const toolbarModelUid = getToolbarModelUidFromNode(activatorNode);
|
|
66
70
|
|
|
67
71
|
if (!ownerDocument || !toolbarModelUid) {
|
|
68
72
|
return activatorNode;
|
|
@@ -95,6 +99,7 @@ export const DragHandler: FC<{ model: FlowModel; children?: React.ReactNode }> =
|
|
|
95
99
|
const dragHandlerRef = useRef<HTMLSpanElement | null>(null);
|
|
96
100
|
const draggableNodeRef = useRef<HTMLElement | null>(null);
|
|
97
101
|
const pointerPressCleanupRef = useRef<(() => void) | null>(null);
|
|
102
|
+
const toolbarDragModelUidRef = useRef<string | null>(null);
|
|
98
103
|
const isDraggingRef = useRef(isDragging);
|
|
99
104
|
const isPointerPressActiveRef = useRef(false);
|
|
100
105
|
const isToolbarDragActiveRef = useRef(false);
|
|
@@ -126,9 +131,13 @@ export const DragHandler: FC<{ model: FlowModel; children?: React.ReactNode }> =
|
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
const toolbarModelUid =
|
|
135
|
+
getToolbarModelUidFromNode(dragHandlerRef.current) || toolbarDragModelUidRef.current || model.uid;
|
|
136
|
+
toolbarDragModelUidRef.current = active ? toolbarModelUid : null;
|
|
137
|
+
|
|
129
138
|
ownerDocument.dispatchEvent(
|
|
130
139
|
new CustomEvent(TOOLBAR_DRAG_ACTIVITY_EVENT, {
|
|
131
|
-
detail: { active, modelUid:
|
|
140
|
+
detail: { active, modelUid: toolbarModelUid },
|
|
132
141
|
}),
|
|
133
142
|
);
|
|
134
143
|
},
|
|
@@ -11,6 +11,7 @@ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
|
|
11
11
|
import { createPortal } from 'react-dom';
|
|
12
12
|
import { Alert, Space } from 'antd';
|
|
13
13
|
import { css } from '@emotion/css';
|
|
14
|
+
import { useMemoizedFn } from 'ahooks';
|
|
14
15
|
import { FlowModel } from '../../../../models';
|
|
15
16
|
import { ToolbarItemConfig } from '../../../../types';
|
|
16
17
|
import { useFlowModelById } from '../../../../hooks';
|
|
@@ -394,6 +395,49 @@ const isModelByIdProps = (props: FlowsFloatContextMenuProps): props is ModelById
|
|
|
394
395
|
return 'uid' in props && 'modelClassName' in props && Boolean(props.uid) && Boolean(props.modelClassName);
|
|
395
396
|
};
|
|
396
397
|
|
|
398
|
+
const stopResizeInteractionEvent = (event?: MouseEvent | React.MouseEvent) => {
|
|
399
|
+
if (!event) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
event.stopPropagation();
|
|
405
|
+
|
|
406
|
+
const nativeEvent = 'nativeEvent' in event ? event.nativeEvent : event;
|
|
407
|
+
nativeEvent.stopImmediatePropagation?.();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const pendingResizeClickSuppressions = new WeakMap<
|
|
411
|
+
Document,
|
|
412
|
+
{ listener: (event: MouseEvent) => void; timer: number }
|
|
413
|
+
>();
|
|
414
|
+
|
|
415
|
+
const clearPendingResizeClickSuppression = (ownerDocument: Document) => {
|
|
416
|
+
const pending = pendingResizeClickSuppressions.get(ownerDocument);
|
|
417
|
+
if (!pending) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
ownerDocument.removeEventListener('click', pending.listener, true);
|
|
422
|
+
(ownerDocument.defaultView || window).clearTimeout(pending.timer);
|
|
423
|
+
pendingResizeClickSuppressions.delete(ownerDocument);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const suppressNextResizeClick = (ownerDocument: Document) => {
|
|
427
|
+
clearPendingResizeClickSuppression(ownerDocument);
|
|
428
|
+
|
|
429
|
+
const listener = (event: MouseEvent) => {
|
|
430
|
+
stopResizeInteractionEvent(event);
|
|
431
|
+
clearPendingResizeClickSuppression(ownerDocument);
|
|
432
|
+
};
|
|
433
|
+
const timer = (ownerDocument.defaultView || window).setTimeout(() => {
|
|
434
|
+
clearPendingResizeClickSuppression(ownerDocument);
|
|
435
|
+
}, 0);
|
|
436
|
+
|
|
437
|
+
pendingResizeClickSuppressions.set(ownerDocument, { listener, timer });
|
|
438
|
+
ownerDocument.addEventListener('click', listener, true);
|
|
439
|
+
};
|
|
440
|
+
|
|
397
441
|
/**
|
|
398
442
|
* FlowsFloatContextMenu组件 - 悬浮配置工具栏组件
|
|
399
443
|
*
|
|
@@ -442,55 +486,80 @@ const ResizeHandles: React.FC<{
|
|
|
442
486
|
const isDraggingRef = useRef<boolean>(false);
|
|
443
487
|
const dragTypeRef = useRef<'left' | 'right' | null>(null);
|
|
444
488
|
const dragStartPosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
489
|
+
const dragOwnerDocumentRef = useRef<Document | null>(null);
|
|
445
490
|
const { onDragStart, onDragEnd } = props;
|
|
446
491
|
|
|
447
492
|
// 把拖拽位移转成上层已约定的 resize 事件。
|
|
448
|
-
const handleDragMove =
|
|
449
|
-
(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
)
|
|
493
|
+
const handleDragMove = useMemoizedFn((e: MouseEvent) => {
|
|
494
|
+
if (!isDraggingRef.current || !dragTypeRef.current) return;
|
|
495
|
+
|
|
496
|
+
stopResizeInteractionEvent(e);
|
|
497
|
+
const deltaX = e.clientX - dragStartPosRef.current.x;
|
|
498
|
+
|
|
499
|
+
switch (dragTypeRef.current) {
|
|
500
|
+
case 'left':
|
|
501
|
+
props.model.parent.emitter.emit('onResizeLeft', { resizeDistance: -deltaX, model: props.model });
|
|
502
|
+
break;
|
|
503
|
+
case 'right':
|
|
504
|
+
props.model.parent.emitter.emit('onResizeRight', { resizeDistance: deltaX, model: props.model });
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const handleDragEnd = useMemoizedFn((e?: MouseEvent) => {
|
|
510
|
+
if (!isDraggingRef.current) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
stopResizeInteractionEvent(e);
|
|
515
|
+
const ownerDocument = dragOwnerDocumentRef.current || document;
|
|
516
|
+
|
|
517
|
+
ownerDocument.removeEventListener('mousemove', handleDragMove, true);
|
|
518
|
+
ownerDocument.removeEventListener('mouseup', handleDragEnd, true);
|
|
519
|
+
suppressNextResizeClick(ownerDocument);
|
|
465
520
|
|
|
466
|
-
const handleDragEnd = useCallback(() => {
|
|
467
521
|
isDraggingRef.current = false;
|
|
468
522
|
dragTypeRef.current = null;
|
|
469
523
|
dragStartPosRef.current = { x: 0, y: 0 };
|
|
470
|
-
|
|
471
|
-
document.removeEventListener('mousemove', handleDragMove);
|
|
472
|
-
document.removeEventListener('mouseup', handleDragEnd);
|
|
524
|
+
dragOwnerDocumentRef.current = null;
|
|
473
525
|
|
|
474
526
|
props.model.parent.emitter.emit('onResizeEnd');
|
|
475
527
|
onDragEnd?.();
|
|
476
|
-
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
return () => {
|
|
532
|
+
const dragOwnerDocument = dragOwnerDocumentRef.current;
|
|
533
|
+
dragOwnerDocument?.removeEventListener('mousemove', handleDragMove, true);
|
|
534
|
+
dragOwnerDocument?.removeEventListener('mouseup', handleDragEnd, true);
|
|
535
|
+
|
|
536
|
+
if (isDraggingRef.current) {
|
|
537
|
+
suppressNextResizeClick(dragOwnerDocument || document);
|
|
538
|
+
props.model.parent.emitter.emit('onResizeEnd');
|
|
539
|
+
onDragEnd?.();
|
|
540
|
+
}
|
|
477
541
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
542
|
+
isDraggingRef.current = false;
|
|
543
|
+
dragTypeRef.current = null;
|
|
544
|
+
dragStartPosRef.current = { x: 0, y: 0 };
|
|
545
|
+
dragOwnerDocumentRef.current = null;
|
|
546
|
+
};
|
|
547
|
+
}, [handleDragMove, handleDragEnd, onDragEnd, props.model]);
|
|
482
548
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
549
|
+
const handleDragStart = useMemoizedFn((e: React.MouseEvent, type: 'left' | 'right') => {
|
|
550
|
+
stopResizeInteractionEvent(e);
|
|
551
|
+
const ownerDocument = e.currentTarget.ownerDocument;
|
|
486
552
|
|
|
487
|
-
|
|
488
|
-
|
|
553
|
+
isDraggingRef.current = true;
|
|
554
|
+
dragTypeRef.current = type;
|
|
555
|
+
dragStartPosRef.current = { x: e.clientX, y: e.clientY };
|
|
556
|
+
dragOwnerDocumentRef.current = ownerDocument;
|
|
489
557
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
558
|
+
ownerDocument.addEventListener('mousemove', handleDragMove, true);
|
|
559
|
+
ownerDocument.addEventListener('mouseup', handleDragEnd, true);
|
|
560
|
+
|
|
561
|
+
onDragStart?.();
|
|
562
|
+
});
|
|
494
563
|
|
|
495
564
|
return (
|
|
496
565
|
<>
|
|
@@ -601,6 +670,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
|
|
|
601
670
|
getPopupContainer,
|
|
602
671
|
handleSettingsMenuOpenChange,
|
|
603
672
|
model,
|
|
673
|
+
modelUid,
|
|
604
674
|
settingsMenuLevel,
|
|
605
675
|
showCopyUidButton,
|
|
606
676
|
showDeleteButton,
|