@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.
Files changed (30) hide show
  1. package/lib/JSRunner.d.ts +1 -0
  2. package/lib/JSRunner.js +110 -20
  3. package/lib/components/dnd/index.js +9 -2
  4. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
  5. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
  6. package/lib/flowContext.js +65 -30
  7. package/lib/runjs-context/helpers.js +12 -5
  8. package/lib/utils/index.d.ts +0 -1
  9. package/lib/utils/index.js +0 -11
  10. package/lib/utils/resolveRunJSObjectValues.js +3 -2
  11. package/lib/utils/runjsModuleLoader.js +0 -30
  12. package/package.json +4 -4
  13. package/src/JSRunner.ts +112 -25
  14. package/src/__tests__/JSRunner.test.ts +4 -5
  15. package/src/__tests__/flowContext.test.ts +43 -0
  16. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  17. package/src/components/dnd/index.tsx +11 -2
  18. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
  19. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
  20. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
  21. package/src/flowContext.ts +68 -26
  22. package/src/runjs-context/helpers.ts +12 -6
  23. package/src/utils/index.ts +0 -9
  24. package/src/utils/resolveRunJSObjectValues.ts +5 -2
  25. package/src/utils/runjsModuleLoader.ts +0 -32
  26. package/lib/utils/safeGlobals.d.ts +0 -28
  27. package/lib/utils/safeGlobals.js +0 -367
  28. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
  29. package/src/utils/__tests__/safeGlobals.test.ts +0 -106
  30. 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.5",
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.5",
12
- "@nocobase/shared": "2.2.0-beta.5",
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": "81eab2cfa9d3d989e99ba0c914807b38db55f023"
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: Record<string, any> = {};
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: createSafeWindow(),
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: createSafeWindow(),
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: createSafeWindow(),
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: createSafeWindow(),
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 = floatToolbarContainer?.getAttribute('data-model-uid');
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: model.uid },
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 = useCallback(
449
- (e: MouseEvent) => {
450
- if (!isDraggingRef.current || !dragTypeRef.current) return;
451
-
452
- const deltaX = e.clientX - dragStartPosRef.current.x;
453
-
454
- switch (dragTypeRef.current) {
455
- case 'left':
456
- props.model.parent.emitter.emit('onResizeLeft', { resizeDistance: -deltaX, model: props.model });
457
- break;
458
- case 'right':
459
- props.model.parent.emitter.emit('onResizeRight', { resizeDistance: deltaX, model: props.model });
460
- break;
461
- }
462
- },
463
- [props.model],
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
- }, [handleDragMove, onDragEnd, props.model]);
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
- const handleDragStart = useCallback(
479
- (e: React.MouseEvent, type: 'left' | 'right') => {
480
- e.preventDefault();
481
- e.stopPropagation();
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
- isDraggingRef.current = true;
484
- dragTypeRef.current = type;
485
- dragStartPosRef.current = { x: e.clientX, y: e.clientY };
549
+ const handleDragStart = useMemoizedFn((e: React.MouseEvent, type: 'left' | 'right') => {
550
+ stopResizeInteractionEvent(e);
551
+ const ownerDocument = e.currentTarget.ownerDocument;
486
552
 
487
- document.addEventListener('mousemove', handleDragMove);
488
- document.addEventListener('mouseup', handleDragEnd);
553
+ isDraggingRef.current = true;
554
+ dragTypeRef.current = type;
555
+ dragStartPosRef.current = { x: e.clientX, y: e.clientY };
556
+ dragOwnerDocumentRef.current = ownerDocument;
489
557
 
490
- onDragStart?.();
491
- },
492
- [handleDragMove, handleDragEnd, onDragStart],
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,