@nocobase/flow-engine 2.1.0-alpha.20 → 2.1.0-alpha.22

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.
@@ -61,11 +61,12 @@ const FlowModelRendererWithAutoFlows = (0, import_reactive.observer)(
61
61
  showErrorFallback,
62
62
  settingsMenuLevel,
63
63
  extraToolbarItems,
64
- fallback
64
+ fallback,
65
+ useCache
65
66
  }) => {
66
67
  const { loading: pending, error: autoFlowsError } = (0, import_hooks.useApplyAutoFlows)(model, inputArgs, {
67
68
  throwOnError: false,
68
- useCache: model.context.useCache
69
+ useCache
69
70
  });
70
71
  (0, import_utils.setAutoFlowError)(model, autoFlowsError || null);
71
72
  if (pending) {
@@ -195,13 +196,15 @@ const FlowModelRenderer = (0, import_reactive.observer)(
195
196
  extraToolbarItems,
196
197
  useCache
197
198
  }) => {
199
+ var _a;
200
+ const resolvedUseCache = typeof useCache === "boolean" ? useCache : (_a = model == null ? void 0 : model.context) == null ? void 0 : _a.useCache;
198
201
  (0, import_react.useEffect)(() => {
199
- if (model == null ? void 0 : model.context) {
202
+ if ((model == null ? void 0 : model.context) && typeof resolvedUseCache !== "undefined") {
200
203
  model.context.defineProperty("useCache", {
201
- value: typeof useCache === "boolean" ? useCache : model.context.useCache
204
+ value: resolvedUseCache
202
205
  });
203
206
  }
204
- }, [model == null ? void 0 : model.context, useCache]);
207
+ }, [model == null ? void 0 : model.context, resolvedUseCache]);
205
208
  if (!model || typeof model.render !== "function") {
206
209
  console.warn("FlowModelRenderer: Invalid model or render method not found.", model);
207
210
  return null;
@@ -218,7 +221,8 @@ const FlowModelRenderer = (0, import_reactive.observer)(
218
221
  showErrorFallback,
219
222
  settingsMenuLevel,
220
223
  extraToolbarItems,
221
- fallback
224
+ fallback,
225
+ useCache: resolvedUseCache
222
226
  }
223
227
  );
224
228
  if (showErrorFallback) {
@@ -52,6 +52,13 @@ var import_reactive = require("../../../../reactive");
52
52
  var import_useFloatToolbarPortal = require("./useFloatToolbarPortal");
53
53
  var import_useFloatToolbarVisibility = require("./useFloatToolbarVisibility");
54
54
  const TOOLBAR_Z_INDEX = 999;
55
+ const getFloatMenuInstanceId = /* @__PURE__ */ __name((model) => {
56
+ if (!model) {
57
+ return "";
58
+ }
59
+ const forkId = (model == null ? void 0 : model.isFork) ? model == null ? void 0 : model.forkId : void 0;
60
+ return forkId == null || forkId === "" ? String(model.uid || "") : `${String(model.uid || "")}::${String(forkId)}`;
61
+ }, "getFloatMenuInstanceId");
55
62
  const hostContainerStyles = import_css.css`
56
63
  position: relative;
57
64
 
@@ -247,7 +254,7 @@ const detectButtonInDOM = /* @__PURE__ */ __name((container) => {
247
254
  }
248
255
  return false;
249
256
  }, "detectButtonInDOM");
250
- const renderToolbarItems = /* @__PURE__ */ __name((model, showDeleteButton, showCopyUidButton, flowEngine, settingsMenuLevel, extraToolbarItems, onSettingsMenuOpenChange, getPopupContainer) => {
257
+ const renderToolbarItems = /* @__PURE__ */ __name((model, modelInstanceId, showDeleteButton, showCopyUidButton, flowEngine, settingsMenuLevel, extraToolbarItems, onSettingsMenuOpenChange, getPopupContainer) => {
251
258
  var _a, _b;
252
259
  const toolbarItems = ((_b = (_a = flowEngine == null ? void 0 : flowEngine.flowSettings) == null ? void 0 : _a.getToolbarItems) == null ? void 0 : _b.call(_a)) || [];
253
260
  const allToolbarItems = [...toolbarItems, ...extraToolbarItems || []];
@@ -262,7 +269,7 @@ const renderToolbarItems = /* @__PURE__ */ __name((model, showDeleteButton, show
262
269
  {
263
270
  key: itemConfig.key,
264
271
  model,
265
- id: model.uid,
272
+ id: modelInstanceId,
266
273
  showDeleteButton,
267
274
  showCopyUidButton,
268
275
  menuLevels: settingsMenuLevel,
@@ -397,7 +404,7 @@ const FlowsFloatContextMenuWithModel = (0, import_reactive.observer)(
397
404
  schedulePortalRectUpdate: /* @__PURE__ */ __name(() => {
398
405
  }, "schedulePortalRectUpdate")
399
406
  });
400
- const modelUid = (model == null ? void 0 : model.uid) || "";
407
+ const modelUid = getFloatMenuInstanceId(model);
401
408
  const flowEngine = (0, import_provider.useFlowEngine)();
402
409
  const updatePortalRectProxy = (0, import_react.useCallback)(() => {
403
410
  portalActionsRef.current.updatePortalRect();
@@ -434,6 +441,7 @@ const FlowsFloatContextMenuWithModel = (0, import_reactive.observer)(
434
441
  const toolbarItems = (0, import_react.useMemo)(
435
442
  () => model ? renderToolbarItems(
436
443
  model,
444
+ modelUid,
437
445
  showDeleteButton,
438
446
  showCopyUidButton,
439
447
  flowEngine,
@@ -495,7 +503,7 @@ const FlowsFloatContextMenuWithModel = (0, import_reactive.observer)(
495
503
  ref: toolbarContainerRef,
496
504
  className: `nb-toolbar-container ${toolbarContainerClassName}`,
497
505
  style: toolbarContainerStyle,
498
- "data-model-uid": model.uid
506
+ "data-model-uid": modelUid
499
507
  },
500
508
  showTitle && (model.title || model.extraTitle) && /* @__PURE__ */ import_react.default.createElement("div", { className: "nb-toolbar-container-title" }, model.title && /* @__PURE__ */ import_react.default.createElement("span", { className: "title-tag" }, model.title), model.extraTitle && /* @__PURE__ */ import_react.default.createElement("span", { className: "title-tag" }, model.extraTitle)),
501
509
  /* @__PURE__ */ import_react.default.createElement(
@@ -528,7 +536,7 @@ const FlowsFloatContextMenuWithModel = (0, import_reactive.observer)(
528
536
  className: `${hostContainerStyles} ${hasButton ? "has-button-child" : ""} ${className || ""}`,
529
537
  style: containerStyle,
530
538
  "data-has-float-menu": "true",
531
- "data-float-menu-model-uid": model.uid,
539
+ "data-float-menu-model-uid": modelUid,
532
540
  onMouseMove: handleChildHover,
533
541
  onMouseEnter: handleHostMouseEnter,
534
542
  onMouseLeave: handleHostMouseLeave
@@ -13,6 +13,7 @@ import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContex
13
13
  import { FlowEngine } from '../flowEngine';
14
14
  import type { ActionDefinition, ArrayElementType, CreateModelOptions, CreateSubModelOptions, DefaultStructure, FlowDefinitionOptions, FlowModelMeta, FlowModelOptions, ModelConstructor, ParamObject, ParentFlowModel, PersistOptions, ResolveUseResult, StepParams } from '../types';
15
15
  import { IModelComponentProps, ReadonlyModelProps } from '../types';
16
+ import type { MenuProps } from 'antd';
16
17
  import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
17
18
  import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
18
19
  import { GlobalFlowRegistry } from '../flow-registry/GlobalFlowRegistry';
@@ -21,7 +22,6 @@ import { FlowSettingsOpenOptions } from '../flowSettings';
21
22
  import type { ScheduleOptions } from '../scheduler/ModelOperationScheduler';
22
23
  import type { DispatchEventOptions, EventDefinition } from '../types';
23
24
  import { ForkFlowModel } from './forkFlowModel';
24
- import type { MenuProps } from 'antd';
25
25
  type BaseMenuItem = NonNullable<MenuProps['items']>[number];
26
26
  type MenuLeafItem = Exclude<BaseMenuItem, {
27
27
  children: MenuProps['items'];
@@ -239,6 +239,7 @@ export declare class FlowModel<Structure extends DefaultStructure = DefaultStruc
239
239
  * 使用 lodash debounce 避免频繁调用
240
240
  */
241
241
  private _rerunLastAutoRun;
242
+ private resetAutoRunState;
242
243
  /**
243
244
  * 通用事件分发钩子:开始
244
245
  * 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
@@ -61,12 +61,12 @@ var import_InstanceFlowRegistry = require("../flow-registry/InstanceFlowRegistry
61
61
  var import_flowContext = require("../flowContext");
62
62
  var import_utils = require("../utils");
63
63
  var import_antd = require("antd");
64
+ var import__ = require("..");
64
65
  var import_ModelActionRegistry = require("../action-registry/ModelActionRegistry");
65
66
  var import_utils2 = require("../components/subModel/utils");
66
67
  var import_ModelEventRegistry = require("../event-registry/ModelEventRegistry");
67
68
  var import_GlobalFlowRegistry = require("../flow-registry/GlobalFlowRegistry");
68
69
  var import_forkFlowModel = require("./forkFlowModel");
69
- var import__ = require("..");
70
70
  var _flowContext;
71
71
  const classActionRegistries = /* @__PURE__ */ new WeakMap();
72
72
  const classEventRegistries = /* @__PURE__ */ new WeakMap();
@@ -191,10 +191,13 @@ const _FlowModel = class _FlowModel {
191
191
  if (changed.type === "set" && import_lodash.default.isEqual(changed.value, changed.oldValue)) {
192
192
  return;
193
193
  }
194
+ const hasLastAutoRun = !!this._lastAutoRunParams;
194
195
  if (this.flowEngine) {
195
196
  this.invalidateFlowCache("beforeRender");
196
197
  }
197
- this._rerunLastAutoRun();
198
+ if (hasLastAutoRun) {
199
+ this._rerunLastAutoRun();
200
+ }
198
201
  this.forks.forEach((fork) => {
199
202
  fork.rerender();
200
203
  });
@@ -682,6 +685,11 @@ const _FlowModel = class _FlowModel {
682
685
  }, "isMatch");
683
686
  return Array.from(allFlows.values()).filter(isMatch);
684
687
  }
688
+ resetAutoRunState() {
689
+ var _a, _b;
690
+ (_b = (_a = this._rerunLastAutoRun) == null ? void 0 : _a.cancel) == null ? void 0 : _b.call(_a);
691
+ this._lastAutoRunParams = null;
692
+ }
685
693
  /**
686
694
  * 通用事件分发钩子:开始
687
695
  * 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
@@ -771,6 +779,7 @@ const _FlowModel = class _FlowModel {
771
779
  }));
772
780
  return () => {
773
781
  var _a3, _b3;
782
+ renderTarget.resetAutoRunState();
774
783
  if (typeof renderTarget.onUnmount === "function") {
775
784
  renderTarget.onUnmount();
776
785
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.1.0-alpha.20",
3
+ "version": "2.1.0-alpha.22",
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.0-alpha.20",
12
- "@nocobase/shared": "2.1.0-alpha.20",
11
+ "@nocobase/sdk": "2.1.0-alpha.22",
12
+ "@nocobase/shared": "2.1.0-alpha.22",
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": "3d1535db6bf93ca23257faf474afee0d565f54c6"
40
+ "gitHead": "81ed83f158f172cca607b36beaf8428b14ba16ad"
41
41
  }
@@ -127,6 +127,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
127
127
  settingsMenuLevel?: number;
128
128
  extraToolbarItems?: ToolbarItemConfig[];
129
129
  fallback?: React.ReactNode;
130
+ useCache?: boolean;
130
131
  }> = observer(
131
132
  ({
132
133
  model,
@@ -139,12 +140,12 @@ const FlowModelRendererWithAutoFlows: React.FC<{
139
140
  settingsMenuLevel,
140
141
  extraToolbarItems,
141
142
  fallback,
143
+ useCache,
142
144
  }) => {
143
145
  // hidden 占位由模型自身处理;无需在此注入
144
-
145
146
  const { loading: pending, error: autoFlowsError } = useApplyAutoFlows(model, inputArgs, {
146
147
  throwOnError: false,
147
- useCache: model.context.useCache,
148
+ useCache,
148
149
  });
149
150
  // 将错误下沉到 model 实例上,供内容层读取(类型安全的 WeakMap 存储)
150
151
  setAutoFlowError(model, autoFlowsError || null);
@@ -348,13 +349,15 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
348
349
  extraToolbarItems,
349
350
  useCache,
350
351
  }) => {
352
+ const resolvedUseCache = typeof useCache === 'boolean' ? useCache : model?.context?.useCache;
353
+
351
354
  useEffect(() => {
352
- if (model?.context) {
355
+ if (model?.context && typeof resolvedUseCache !== 'undefined') {
353
356
  model.context.defineProperty('useCache', {
354
- value: typeof useCache === 'boolean' ? useCache : model.context.useCache,
357
+ value: resolvedUseCache,
355
358
  });
356
359
  }
357
- }, [model?.context, useCache]);
360
+ }, [model?.context, resolvedUseCache]);
358
361
 
359
362
  if (!model || typeof model.render !== 'function') {
360
363
  // 可以选择渲染 null 或者一个错误/提示信息
@@ -375,6 +378,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
375
378
  settingsMenuLevel={settingsMenuLevel}
376
379
  extraToolbarItems={extraToolbarItems}
377
380
  fallback={fallback}
381
+ useCache={resolvedUseCache}
378
382
  />
379
383
  );
380
384
 
@@ -38,9 +38,8 @@ describe('FlowModelRenderer', () => {
38
38
  test('should pass useCache to useApplyAutoFlows and set it on context', async () => {
39
39
  const { unmount } = renderWithProvider(<FlowModelRenderer model={model} useCache={true} />);
40
40
 
41
- // Check if dispatchEvent was called with useCache: true
42
- // useApplyAutoFlows calls dispatchEvent('beforeRender', inputArgs, { useCache })
43
41
  await waitFor(() => {
42
+ expect(model.dispatchEvent).toHaveBeenCalledTimes(1);
44
43
  expect(model.dispatchEvent).toHaveBeenCalledWith(
45
44
  'beforeRender',
46
45
  undefined,
@@ -58,6 +57,7 @@ describe('FlowModelRenderer', () => {
58
57
  const { unmount } = renderWithProvider(<FlowModelRenderer model={model} useCache={false} />);
59
58
 
60
59
  await waitFor(() => {
60
+ expect(model.dispatchEvent).toHaveBeenCalledTimes(1);
61
61
  expect(model.dispatchEvent).toHaveBeenCalledWith(
62
62
  'beforeRender',
63
63
  undefined,
@@ -74,6 +74,7 @@ describe('FlowModelRenderer', () => {
74
74
  const { unmount } = renderWithProvider(<FlowModelRenderer model={model} />);
75
75
 
76
76
  await waitFor(() => {
77
+ expect(model.dispatchEvent).toHaveBeenCalledTimes(1);
77
78
  expect(model.dispatchEvent).toHaveBeenCalledWith(
78
79
  'beforeRender',
79
80
  undefined,
@@ -86,4 +87,66 @@ describe('FlowModelRenderer', () => {
86
87
 
87
88
  unmount();
88
89
  });
90
+
91
+ test('should clear stale beforeRender state after unmount when reusing the same model', async () => {
92
+ const statefulEngine = new FlowEngine();
93
+ const onMountSpy = vi.fn();
94
+ const onUnmountSpy = vi.fn();
95
+
96
+ class StatefulModel extends FlowModel {
97
+ render(): any {
98
+ return <div>Stateful Content</div>;
99
+ }
100
+
101
+ protected onMount(): void {
102
+ onMountSpy();
103
+ }
104
+
105
+ protected onUnmount(): void {
106
+ onUnmountSpy();
107
+ }
108
+ }
109
+
110
+ const statefulModel = new StatefulModel({
111
+ uid: 'stateful-model',
112
+ flowEngine: statefulEngine,
113
+ });
114
+ const executorSpy = vi.spyOn((statefulEngine as any).executor, 'dispatchEvent').mockResolvedValue([]);
115
+
116
+ const firstRender = renderWithProvider(<FlowModelRenderer model={statefulModel} />);
117
+ await waitFor(() => {
118
+ expect(executorSpy).toHaveBeenCalledTimes(1);
119
+ });
120
+ await waitFor(() => {
121
+ expect(onMountSpy).toHaveBeenCalledTimes(1);
122
+ });
123
+
124
+ firstRender.unmount();
125
+ await waitFor(() => {
126
+ expect(onUnmountSpy).toHaveBeenCalledTimes(1);
127
+ });
128
+
129
+ executorSpy.mockClear();
130
+ statefulModel.setStepParams('anyFlow', 'anyStep', { x: 1 });
131
+ await new Promise((resolve) => setTimeout(resolve, 150));
132
+ expect(executorSpy.mock.calls.length).toBe(0);
133
+
134
+ const secondRender = renderWithProvider(<FlowModelRenderer model={statefulModel} />);
135
+ await waitFor(() => {
136
+ expect(executorSpy).toHaveBeenCalledTimes(1);
137
+ });
138
+ await waitFor(() => {
139
+ expect(onMountSpy).toHaveBeenCalledTimes(2);
140
+ });
141
+ const [target, eventName, inputArgs, options] = executorSpy.mock.calls[0];
142
+ expect(target).toBe(statefulModel);
143
+ expect(eventName).toBe('beforeRender');
144
+ expect(inputArgs).toBeUndefined();
145
+ expect(options).toMatchObject({ useCache: true });
146
+
147
+ secondRender.unmount();
148
+ await waitFor(() => {
149
+ expect(onUnmountSpy).toHaveBeenCalledTimes(2);
150
+ });
151
+ });
89
152
  });
@@ -70,6 +70,15 @@ interface BaseFloatContextMenuProps {
70
70
  toolbarPosition?: ToolbarPosition;
71
71
  }
72
72
 
73
+ const getFloatMenuInstanceId = (model?: FlowModel | null) => {
74
+ if (!model) {
75
+ return '';
76
+ }
77
+
78
+ const forkId = (model as any)?.isFork ? (model as any)?.forkId : undefined;
79
+ return forkId == null || forkId === '' ? String(model.uid || '') : `${String(model.uid || '')}::${String(forkId)}`;
80
+ };
81
+
73
82
  const hostContainerStyles = css`
74
83
  position: relative;
75
84
 
@@ -279,6 +288,7 @@ const detectButtonInDOM = (container: HTMLElement): boolean => {
279
288
  // 渲染工具栏项目,并让设置菜单与工具栏共享同一个 popup 容器。
280
289
  const renderToolbarItems = (
281
290
  model: FlowModel,
291
+ modelInstanceId: string,
282
292
  showDeleteButton: boolean,
283
293
  showCopyUidButton: boolean,
284
294
  flowEngine: FlowEngine,
@@ -304,7 +314,7 @@ const renderToolbarItems = (
304
314
  <ItemComponent
305
315
  key={itemConfig.key}
306
316
  model={model}
307
- id={model.uid}
317
+ id={modelInstanceId}
308
318
  showDeleteButton={showDeleteButton}
309
319
  showCopyUidButton={showCopyUidButton}
310
320
  menuLevels={settingsMenuLevel}
@@ -517,7 +527,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
517
527
  updatePortalRect: () => {},
518
528
  schedulePortalRectUpdate: () => {},
519
529
  });
520
- const modelUid = model?.uid || '';
530
+ const modelUid = getFloatMenuInstanceId(model);
521
531
  const flowEngine = useFlowEngine();
522
532
  const updatePortalRectProxy = useCallback(() => {
523
533
  portalActionsRef.current.updatePortalRect();
@@ -559,6 +569,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
559
569
  model
560
570
  ? renderToolbarItems(
561
571
  model,
572
+ modelUid,
562
573
  showDeleteButton,
563
574
  showCopyUidButton,
564
575
  flowEngine,
@@ -629,7 +640,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
629
640
  ref={toolbarContainerRef}
630
641
  className={`nb-toolbar-container ${toolbarContainerClassName}`}
631
642
  style={toolbarContainerStyle}
632
- data-model-uid={model.uid}
643
+ data-model-uid={modelUid}
633
644
  >
634
645
  {showTitle && (model.title || model.extraTitle) && (
635
646
  <div className="nb-toolbar-container-title">
@@ -668,7 +679,7 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
668
679
  className={`${hostContainerStyles} ${hasButton ? 'has-button-child' : ''} ${className || ''}`}
669
680
  style={containerStyle}
670
681
  data-has-float-menu="true"
671
- data-float-menu-model-uid={model.uid}
682
+ data-float-menu-model-uid={modelUid}
672
683
  onMouseMove={handleChildHover}
673
684
  onMouseEnter={handleHostMouseEnter}
674
685
  onMouseLeave={handleHostMouseLeave}
@@ -544,4 +544,66 @@ describe('FlowsFloatContextMenu', () => {
544
544
  expect(parentOverlayAfterRestore?.className).toContain('nb-toolbar-visible');
545
545
  });
546
546
  });
547
+
548
+ it('treats forked models as distinct float menu instances even when they share the same uid', async () => {
549
+ const engine = new FlowEngine();
550
+ await engine.flowSettings.forceEnable();
551
+ const masterModel = new FlowModel({ uid: 'forked-model', flowEngine: engine });
552
+ masterModel.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
553
+ masterModel.render = vi.fn(function (this: any) {
554
+ return <div data-testid={`content-${String(this.forkId || this.uid)}`}>{String(this.forkId || this.uid)}</div>;
555
+ });
556
+
557
+ const firstFork = masterModel.createFork({}, 'card-1') as FlowModel & { forkId?: string };
558
+ const secondFork = masterModel.createFork({}, 'card-2') as FlowModel & { forkId?: string };
559
+ const firstInstanceId = `forked-model::${String((firstFork as any).forkId)}`;
560
+ const secondInstanceId = `forked-model::${String((secondFork as any).forkId)}`;
561
+ const appContainer = createAppContainer();
562
+ mockRect(appContainer, { top: 0, left: 0, width: 1280, height: 900 });
563
+
564
+ const { getByTestId } = renderWithProviders(
565
+ engine,
566
+ <>
567
+ <FlowsFloatContextMenu model={firstFork}>
568
+ <div data-testid="fork-host-1">first</div>
569
+ </FlowsFloatContextMenu>
570
+ <FlowsFloatContextMenu model={secondFork}>
571
+ <div data-testid="fork-host-2">second</div>
572
+ </FlowsFloatContextMenu>
573
+ </>,
574
+ { container: appContainer },
575
+ );
576
+
577
+ const firstHost = getHost(getByTestId('fork-host-1'));
578
+ const secondHost = getHost(getByTestId('fork-host-2'));
579
+ mockRect(firstHost, { top: 20, left: 20, width: 180, height: 72 });
580
+ mockRect(secondHost, { top: 120, left: 20, width: 180, height: 72 });
581
+
582
+ fireEvent.mouseEnter(firstHost);
583
+
584
+ const firstOverlay = await waitFor(() => {
585
+ const nextOverlay = queryOverlay(appContainer, firstInstanceId);
586
+ expect(nextOverlay).toBeTruthy();
587
+ return nextOverlay as HTMLDivElement;
588
+ });
589
+
590
+ await waitFor(() => {
591
+ expect(within(firstOverlay).getByLabelText('flows-settings')).toBeTruthy();
592
+ });
593
+
594
+ fireEvent.mouseEnter(secondHost);
595
+
596
+ const secondOverlay = await waitFor(() => {
597
+ const nextOverlay = queryOverlay(appContainer, secondInstanceId);
598
+ expect(nextOverlay).toBeTruthy();
599
+ return nextOverlay as HTMLDivElement;
600
+ });
601
+
602
+ await waitFor(() => {
603
+ expect(within(secondOverlay).getByLabelText('flows-settings')).toBeTruthy();
604
+ });
605
+
606
+ expect(firstOverlay.getAttribute('data-model-uid')).toBe(firstInstanceId);
607
+ expect(secondOverlay.getAttribute('data-model-uid')).toBe(secondInstanceId);
608
+ });
547
609
  });
@@ -35,6 +35,8 @@ import { IModelComponentProps, ReadonlyModelProps } from '../types';
35
35
  import { isInheritedFrom, setupRuntimeContextSteps } from '../utils';
36
36
  // import { FlowExitAllException } from '../utils/exceptions';
37
37
  import { Typography } from 'antd';
38
+ import type { MenuProps } from 'antd';
39
+ import { observer } from '..';
38
40
  import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
39
41
  import { buildSubModelItem } from '../components/subModel/utils';
40
42
  import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
@@ -44,8 +46,6 @@ import { FlowSettingsOpenOptions } from '../flowSettings';
44
46
  import type { ScheduleOptions } from '../scheduler/ModelOperationScheduler';
45
47
  import type { DispatchEventOptions, EventDefinition } from '../types';
46
48
  import { ForkFlowModel } from './forkFlowModel';
47
- import type { MenuProps } from 'antd';
48
- import { observer } from '..';
49
49
 
50
50
  // 使用 WeakMap 为每个类缓存一个 ModelActionRegistry 实例
51
51
  const classActionRegistries = new WeakMap<typeof FlowModel, ModelActionRegistry>();
@@ -216,11 +216,14 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
216
216
  if (changed.type === 'set' && _.isEqual(changed.value, changed.oldValue)) {
217
217
  return;
218
218
  }
219
+ const hasLastAutoRun = !!this._lastAutoRunParams;
219
220
 
220
221
  if (this.flowEngine) {
221
222
  this.invalidateFlowCache('beforeRender');
222
223
  }
223
- this._rerunLastAutoRun();
224
+ if (hasLastAutoRun) {
225
+ this._rerunLastAutoRun();
226
+ }
224
227
  this.forks.forEach((fork) => {
225
228
  fork.rerender();
226
229
  });
@@ -866,6 +869,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
866
869
  }
867
870
  }, 100);
868
871
 
872
+ private resetAutoRunState(): void {
873
+ this._rerunLastAutoRun?.cancel?.();
874
+ this._lastAutoRunParams = null;
875
+ }
876
+
869
877
  /**
870
878
  * 通用事件分发钩子:开始
871
879
  * 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
@@ -959,7 +967,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
959
967
  }
960
968
 
961
969
  // 创建缓存的响应式包装器组件工厂(只创建一次)
962
- const createReactiveWrapper = (modelInstance: any) => {
970
+ const createReactiveWrapper = (modelInstance: FlowModel) => {
963
971
  const ReactiveWrapper = observer(() => {
964
972
  // 触发响应式更新的关键属性访问(读取 run/渲染目标的 props)
965
973
  const renderTarget = modelInstance;
@@ -985,6 +993,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
985
993
  model: renderTarget,
986
994
  });
987
995
  return () => {
996
+ renderTarget.resetAutoRunState();
988
997
  if (typeof renderTarget.onUnmount === 'function') {
989
998
  renderTarget.onUnmount();
990
999
  }