@nocobase/flow-engine 2.2.0-beta.5 → 2.2.0-beta.7
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/JSRunner.d.ts +1 -0
- package/lib/JSRunner.js +110 -20
- 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.js +65 -30
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/utils/index.d.ts +0 -1
- package/lib/utils/index.js +0 -11
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowContext.test.ts +43 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- 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 +68 -26
- package/src/runjs-context/helpers.ts +12 -6
- package/src/utils/index.ts +0 -9
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/lib/utils/safeGlobals.d.ts +0 -28
- package/lib/utils/safeGlobals.js +0 -367
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
- package/src/utils/__tests__/safeGlobals.test.ts +0 -106
- package/src/utils/safeGlobals.ts +0 -406
|
@@ -34,27 +34,6 @@ __export(runjsModuleLoader_exports, {
|
|
|
34
34
|
module.exports = __toCommonJS(runjsModuleLoader_exports);
|
|
35
35
|
var import_runjsLibs = require("../runjsLibs");
|
|
36
36
|
var import_resolveModuleUrl = require("./resolveModuleUrl");
|
|
37
|
-
var import_safeGlobals = require("./safeGlobals");
|
|
38
|
-
function snapshotOwnKeys(obj) {
|
|
39
|
-
try {
|
|
40
|
-
if (!obj || typeof obj !== "object" && typeof obj !== "function") return [];
|
|
41
|
-
return Object.getOwnPropertyNames(obj);
|
|
42
|
-
} catch (_) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
__name(snapshotOwnKeys, "snapshotOwnKeys");
|
|
47
|
-
function diffAddedKeys(afterKeys, beforeKeys) {
|
|
48
|
-
if (!afterKeys.length) return [];
|
|
49
|
-
if (!beforeKeys.length) return [...afterKeys];
|
|
50
|
-
const beforeSet = new Set(beforeKeys);
|
|
51
|
-
const added = [];
|
|
52
|
-
for (const k of afterKeys) {
|
|
53
|
-
if (!beforeSet.has(k)) added.push(k);
|
|
54
|
-
}
|
|
55
|
-
return added;
|
|
56
|
-
}
|
|
57
|
-
__name(diffAddedKeys, "diffAddedKeys");
|
|
58
37
|
async function withRunjsModuleLoadLock(task) {
|
|
59
38
|
const g = globalThis;
|
|
60
39
|
g.__nocobaseRunjsModuleLoadLock = (g.__nocobaseRunjsModuleLoadLock || Promise.resolve()).catch(() => {
|
|
@@ -222,8 +201,6 @@ async function prefetchEsmModule(url, options) {
|
|
|
222
201
|
__name(prefetchEsmModule, "prefetchEsmModule");
|
|
223
202
|
async function runjsRequireAsync(requirejs, url) {
|
|
224
203
|
return await withRunjsModuleLoadLock(async () => {
|
|
225
|
-
const beforeWinKeys = typeof window !== "undefined" ? snapshotOwnKeys(window) : [];
|
|
226
|
-
const beforeDocKeys = typeof document !== "undefined" ? snapshotOwnKeys(document) : [];
|
|
227
204
|
let result;
|
|
228
205
|
let error;
|
|
229
206
|
try {
|
|
@@ -242,13 +219,6 @@ async function runjsRequireAsync(requirejs, url) {
|
|
|
242
219
|
});
|
|
243
220
|
} catch (e) {
|
|
244
221
|
error = e;
|
|
245
|
-
} finally {
|
|
246
|
-
const afterWinKeys = typeof window !== "undefined" ? snapshotOwnKeys(window) : [];
|
|
247
|
-
const afterDocKeys = typeof document !== "undefined" ? snapshotOwnKeys(document) : [];
|
|
248
|
-
const addedWinKeys = diffAddedKeys(afterWinKeys, beforeWinKeys);
|
|
249
|
-
const addedDocKeys = diffAddedKeys(afterDocKeys, beforeDocKeys);
|
|
250
|
-
(0, import_safeGlobals.registerRunJSSafeWindowGlobals)(addedWinKeys);
|
|
251
|
-
(0, import_safeGlobals.registerRunJSSafeDocumentGlobals)(addedDocKeys);
|
|
252
222
|
}
|
|
253
223
|
if (error) throw error;
|
|
254
224
|
return result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.7",
|
|
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.2.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.2.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-beta.7",
|
|
12
|
+
"@nocobase/shared": "2.2.0-beta.7",
|
|
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": "e6a3fa8963a73cd9ddfc1273d71b0012483e1ad8"
|
|
41
41
|
}
|
package/src/JSRunner.ts
CHANGED
|
@@ -39,6 +39,117 @@ export function shouldPreprocessRunJSTemplates(
|
|
|
39
39
|
return options?.version !== 'v2';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const RUNJS_BROWSER_GLOBAL_NAMES = [
|
|
43
|
+
'fetch',
|
|
44
|
+
'localStorage',
|
|
45
|
+
'sessionStorage',
|
|
46
|
+
'XMLHttpRequest',
|
|
47
|
+
'WebSocket',
|
|
48
|
+
'Worker',
|
|
49
|
+
'SharedWorker',
|
|
50
|
+
'ServiceWorker',
|
|
51
|
+
'BroadcastChannel',
|
|
52
|
+
'EventSource',
|
|
53
|
+
'indexedDB',
|
|
54
|
+
'caches',
|
|
55
|
+
'Function',
|
|
56
|
+
'eval',
|
|
57
|
+
'globalThis',
|
|
58
|
+
'Intl',
|
|
59
|
+
'Blob',
|
|
60
|
+
'URL',
|
|
61
|
+
'location',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export const RUNJS_ALLOWED_BARE_GLOBAL_NAMES = [
|
|
65
|
+
'ctx',
|
|
66
|
+
'console',
|
|
67
|
+
'window',
|
|
68
|
+
'document',
|
|
69
|
+
'navigator',
|
|
70
|
+
'setTimeout',
|
|
71
|
+
'clearTimeout',
|
|
72
|
+
'setInterval',
|
|
73
|
+
'clearInterval',
|
|
74
|
+
'Array',
|
|
75
|
+
'ArrayBuffer',
|
|
76
|
+
'BigInt',
|
|
77
|
+
'BigInt64Array',
|
|
78
|
+
'BigUint64Array',
|
|
79
|
+
'Boolean',
|
|
80
|
+
'DataView',
|
|
81
|
+
'Date',
|
|
82
|
+
'Error',
|
|
83
|
+
'EvalError',
|
|
84
|
+
'FinalizationRegistry',
|
|
85
|
+
'Float32Array',
|
|
86
|
+
'Float64Array',
|
|
87
|
+
'Int8Array',
|
|
88
|
+
'Int16Array',
|
|
89
|
+
'Int32Array',
|
|
90
|
+
'Map',
|
|
91
|
+
'Math',
|
|
92
|
+
'Number',
|
|
93
|
+
'Object',
|
|
94
|
+
'Promise',
|
|
95
|
+
'Proxy',
|
|
96
|
+
'RangeError',
|
|
97
|
+
'ReferenceError',
|
|
98
|
+
'Reflect',
|
|
99
|
+
'RegExp',
|
|
100
|
+
'Set',
|
|
101
|
+
'String',
|
|
102
|
+
'Symbol',
|
|
103
|
+
'SyntaxError',
|
|
104
|
+
'TypeError',
|
|
105
|
+
'URIError',
|
|
106
|
+
'Uint8Array',
|
|
107
|
+
'Uint8ClampedArray',
|
|
108
|
+
'Uint16Array',
|
|
109
|
+
'Uint32Array',
|
|
110
|
+
'WeakMap',
|
|
111
|
+
'WeakRef',
|
|
112
|
+
'WeakSet',
|
|
113
|
+
'JSON',
|
|
114
|
+
'decodeURI',
|
|
115
|
+
'decodeURIComponent',
|
|
116
|
+
'encodeURI',
|
|
117
|
+
'encodeURIComponent',
|
|
118
|
+
'isFinite',
|
|
119
|
+
'isNaN',
|
|
120
|
+
'parseFloat',
|
|
121
|
+
'parseInt',
|
|
122
|
+
'undefined',
|
|
123
|
+
'NaN',
|
|
124
|
+
'Infinity',
|
|
125
|
+
...RUNJS_BROWSER_GLOBAL_NAMES,
|
|
126
|
+
] as const;
|
|
127
|
+
|
|
128
|
+
function collectRunJSBrowserGlobals(providedGlobals: Record<string, unknown> = {}) {
|
|
129
|
+
const windowGlobal = providedGlobals.window;
|
|
130
|
+
if (!windowGlobal || typeof windowGlobal !== 'object') {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const windowRecord = windowGlobal as Record<string, unknown>;
|
|
135
|
+
const globals: Record<string, unknown> = {};
|
|
136
|
+
RUNJS_BROWSER_GLOBAL_NAMES.forEach((name) => {
|
|
137
|
+
if (Object.prototype.hasOwnProperty.call(providedGlobals, name)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const value = windowRecord[name];
|
|
142
|
+
if (typeof value === 'undefined') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
globals[name] = name === 'fetch' && typeof value === 'function' ? value.bind(windowGlobal) : value;
|
|
146
|
+
} catch {
|
|
147
|
+
// Ignore browser globals that cannot be read in the current environment.
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return globals;
|
|
151
|
+
}
|
|
152
|
+
|
|
42
153
|
// Heuristic: detect likely bare `{{ctx.xxx}}` usage in executable positions (not quoted string literals).
|
|
43
154
|
const BARE_CTX_TEMPLATE_RE = /(^|[=(:,[\s)])(\{\{\s*(ctx(?:\.|\[|\?\.)[^}]*)\s*\}\})/m;
|
|
44
155
|
|
|
@@ -98,31 +209,7 @@ export class JSRunner {
|
|
|
98
209
|
};
|
|
99
210
|
|
|
100
211
|
const providedGlobals = options.globals || {};
|
|
101
|
-
const liftedGlobals
|
|
102
|
-
|
|
103
|
-
// Auto-lift selected globals from safe window into top-level sandbox globals
|
|
104
|
-
// so user code can access them directly (e.g. `new Blob(...)`).
|
|
105
|
-
if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'Blob')) {
|
|
106
|
-
try {
|
|
107
|
-
const blobCtor = (providedGlobals as any).window?.Blob;
|
|
108
|
-
if (typeof blobCtor !== 'undefined') {
|
|
109
|
-
liftedGlobals.Blob = blobCtor;
|
|
110
|
-
}
|
|
111
|
-
} catch {
|
|
112
|
-
// ignore when window proxy blocks property access
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'URL')) {
|
|
117
|
-
try {
|
|
118
|
-
const urlCtor = (providedGlobals as any).window?.URL;
|
|
119
|
-
if (typeof urlCtor !== 'undefined') {
|
|
120
|
-
liftedGlobals.URL = urlCtor;
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
// ignore when window proxy blocks property access
|
|
124
|
-
}
|
|
125
|
-
}
|
|
212
|
+
const liftedGlobals = collectRunJSBrowserGlobals(providedGlobals);
|
|
126
213
|
|
|
127
214
|
this.globals = {
|
|
128
215
|
console,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
11
|
import { JSRunner, shouldPreprocessRunJSTemplates } from '../JSRunner';
|
|
12
|
-
import { createSafeWindow } from '../utils';
|
|
13
12
|
|
|
14
13
|
describe('JSRunner', () => {
|
|
15
14
|
let originalSearch: string;
|
|
@@ -68,7 +67,7 @@ describe('JSRunner', () => {
|
|
|
68
67
|
|
|
69
68
|
const runner = new JSRunner({
|
|
70
69
|
globals: {
|
|
71
|
-
window
|
|
70
|
+
window,
|
|
72
71
|
},
|
|
73
72
|
});
|
|
74
73
|
|
|
@@ -84,7 +83,7 @@ describe('JSRunner', () => {
|
|
|
84
83
|
|
|
85
84
|
const runner = new JSRunner({
|
|
86
85
|
globals: {
|
|
87
|
-
window
|
|
86
|
+
window,
|
|
88
87
|
Blob: explicitBlob,
|
|
89
88
|
},
|
|
90
89
|
});
|
|
@@ -97,7 +96,7 @@ describe('JSRunner', () => {
|
|
|
97
96
|
it('auto-lifts URL from injected window to top-level globals', async () => {
|
|
98
97
|
const runner = new JSRunner({
|
|
99
98
|
globals: {
|
|
100
|
-
window
|
|
99
|
+
window,
|
|
101
100
|
},
|
|
102
101
|
});
|
|
103
102
|
|
|
@@ -114,7 +113,7 @@ describe('JSRunner', () => {
|
|
|
114
113
|
|
|
115
114
|
const runner = new JSRunner({
|
|
116
115
|
globals: {
|
|
117
|
-
window
|
|
116
|
+
window,
|
|
118
117
|
URL: explicitURL,
|
|
119
118
|
},
|
|
120
119
|
});
|
|
@@ -177,6 +177,49 @@ describe('FlowContext properties and methods', () => {
|
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it('should expose current role as a top-level variable', async () => {
|
|
181
|
+
const engine = new FlowEngine();
|
|
182
|
+
const ctx = engine.context;
|
|
183
|
+
ctx.defineProperty('api', { value: { auth: { role: 'admin' } } });
|
|
184
|
+
|
|
185
|
+
expect(ctx.role).toBe('admin');
|
|
186
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toBe('admin');
|
|
187
|
+
|
|
188
|
+
const roleNode = ctx.getPropertyMetaTree().find((node) => node.name === 'role');
|
|
189
|
+
expect(roleNode).toMatchObject({
|
|
190
|
+
name: 'role',
|
|
191
|
+
title: '{{t("Current role")}}',
|
|
192
|
+
paths: ['role'],
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should expose actual role names for union role mode', async () => {
|
|
197
|
+
const engine = new FlowEngine();
|
|
198
|
+
const ctx = engine.context;
|
|
199
|
+
ctx.defineProperty('api', { value: { auth: { role: '__union__' } } });
|
|
200
|
+
ctx.defineProperty('user', {
|
|
201
|
+
value: {
|
|
202
|
+
roles: [
|
|
203
|
+
{ name: 'admin', title: 'Admin' },
|
|
204
|
+
{ name: 'member', title: 'Member' },
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(ctx.role).toEqual(['admin', 'member']);
|
|
210
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toEqual(['admin', 'member']);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should expose an empty role list for union role mode without user roles', async () => {
|
|
214
|
+
const engine = new FlowEngine();
|
|
215
|
+
const ctx = engine.context;
|
|
216
|
+
ctx.defineProperty('api', { value: { auth: { role: '__union__' } } });
|
|
217
|
+
ctx.defineProperty('user', { value: {} });
|
|
218
|
+
|
|
219
|
+
expect(ctx.role).toEqual([]);
|
|
220
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.role }}')).resolves.toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
|
|
180
223
|
it('should throw sync error in get', () => {
|
|
181
224
|
const ctx = new FlowContext();
|
|
182
225
|
ctx.defineProperty('error', {
|
|
@@ -104,4 +104,32 @@ describe('FlowModelContext.openView - navigation enforcement', () => {
|
|
|
104
104
|
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
105
105
|
expect(child.dispatchEvent.mock.calls[0][0]).toBe('click');
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
it('inherits current model input args when opening an external popup', async () => {
|
|
109
|
+
const { parent, child } = setup();
|
|
110
|
+
parent['getInputArgs'] = vi.fn(() => ({
|
|
111
|
+
filterByTk: 2,
|
|
112
|
+
sourceId: 10,
|
|
113
|
+
defaultInputKeys: ['filterByTk', 'sourceId'],
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
await (parent.context as any).openView('child-uid', { mode: 'dialog' });
|
|
117
|
+
|
|
118
|
+
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(child.dispatchEvent.mock.calls[0][1]).toMatchObject({
|
|
120
|
+
mode: 'dialog',
|
|
121
|
+
filterByTk: 2,
|
|
122
|
+
sourceId: 10,
|
|
123
|
+
});
|
|
124
|
+
expect(child.dispatchEvent.mock.calls[0][1]).not.toHaveProperty('defaultInputKeys');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does not debounce external popup dispatches', async () => {
|
|
128
|
+
const { parent, child } = setup();
|
|
129
|
+
|
|
130
|
+
await (parent.context as any).openView('child-uid', { mode: 'dialog', filterByTk: 1 });
|
|
131
|
+
|
|
132
|
+
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(child.dispatchEvent.mock.calls[0][2]).toBeUndefined();
|
|
134
|
+
});
|
|
107
135
|
});
|
|
@@ -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,
|