@nocobase/flow-engine 2.0.57 → 2.0.59

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.
@@ -153,7 +153,7 @@ const _FlowExecutor = class _FlowExecutor {
153
153
  const stepDefaultParams = await (0, import_utils.resolveDefaultParams)(step.defaultParams, runtimeCtx);
154
154
  combinedParams = { ...stepDefaultParams };
155
155
  } else {
156
- flowContext.logger.error(
156
+ flowContext.logger.warn(
157
157
  `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`
158
158
  );
159
159
  continue;
package/lib/flowEngine.js CHANGED
@@ -63,6 +63,7 @@ var import_emitter = require("./emitter");
63
63
  var import_ModelOperationScheduler = __toESM(require("./scheduler/ModelOperationScheduler"));
64
64
  var import_utils = require("./utils");
65
65
  var _FlowEngine_instances, registerModel_fn;
66
+ const getFlowEngineLoggerLevel = /* @__PURE__ */ __name(() => process.env.NODE_ENV === "production" ? "warn" : "trace", "getFlowEngineLoggerLevel");
66
67
  const _FlowEngine = class _FlowEngine {
67
68
  /**
68
69
  * Constructor. Initializes React view, registers default model and form scopes.
@@ -173,7 +174,7 @@ const _FlowEngine = class _FlowEngine {
173
174
  MultiRecordResource: import_resources.MultiRecordResource
174
175
  });
175
176
  this.logger = (0, import_pino.default)({
176
- level: "trace",
177
+ level: getFlowEngineLoggerLevel(),
177
178
  browser: {
178
179
  write: {
179
180
  fatal: /* @__PURE__ */ __name((o) => console.trace(o), "fatal"),
@@ -510,7 +511,6 @@ const _FlowEngine = class _FlowEngine {
510
511
  const visited = /* @__PURE__ */ new Set();
511
512
  while (current) {
512
513
  if (visited.has(current)) {
513
- console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
514
514
  break;
515
515
  }
516
516
  visited.add(current);
@@ -611,7 +611,7 @@ const _FlowEngine = class _FlowEngine {
611
611
  removeModel(uid) {
612
612
  var _a, _b, _c;
613
613
  if (!this._modelInstances.has(uid)) {
614
- console.warn(`FlowEngine: Model with UID '${uid}' does not exist.`);
614
+ this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
615
615
  return false;
616
616
  }
617
617
  const modelInstance = this._modelInstances.get(uid);
@@ -624,7 +624,7 @@ const _FlowModel = class _FlowModel {
624
624
  }
625
625
  const isFork = this.isFork === true;
626
626
  const target = this;
627
- console.log(
627
+ currentFlowEngine.logger.debug(
628
628
  `[FlowModel] applyFlow: uid=${this.uid}, flowKey=${flowKey}, isFork=${isFork}, cleanRun=${this.cleanRun}, targetIsFork=${(target == null ? void 0 : target.isFork) === true}`
629
629
  );
630
630
  return currentFlowEngine.executor.runFlow(target, flowKey, inputArgs, runId);
@@ -637,7 +637,7 @@ const _FlowModel = class _FlowModel {
637
637
  }
638
638
  const isFork = this.isFork === true;
639
639
  const target = this;
640
- console.log(
640
+ currentFlowEngine.logger.debug(
641
641
  `[FlowModel] dispatchEvent: uid=${this.uid}, event=${eventName}, isFork=${isFork}, cleanRun=${this.cleanRun}, targetIsFork=${(target == null ? void 0 : target.isFork) === true}`
642
642
  );
643
643
  return await currentFlowEngine.executor.dispatchEvent(target, eventName, inputArgs, options);
@@ -1012,7 +1012,7 @@ const _FlowModel = class _FlowModel {
1012
1012
  }
1013
1013
  clearForks() {
1014
1014
  var _a;
1015
- console.log(`FlowModel ${this.uid} clearing all forks.`);
1015
+ this.flowEngine.logger.debug(`FlowModel ${this.uid} clearing all forks.`);
1016
1016
  if ((_a = this.forks) == null ? void 0 : _a.size) {
1017
1017
  this.forks.forEach((fork) => fork.dispose());
1018
1018
  this.forks.clear();
@@ -33,6 +33,75 @@ __export(createViewMeta_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(createViewMeta_exports);
35
35
  var import_variablesParams = require("../utils/variablesParams");
36
+ function isDefined(value) {
37
+ return value !== void 0 && value !== null;
38
+ }
39
+ __name(isDefined, "isDefined");
40
+ function isSameViewParamValue(left, right) {
41
+ if (left === right) return true;
42
+ if (!isDefined(left) || !isDefined(right)) return false;
43
+ try {
44
+ return JSON.stringify(left) === JSON.stringify(right);
45
+ } catch (_) {
46
+ return String(left) === String(right);
47
+ }
48
+ }
49
+ __name(isSameViewParamValue, "isSameViewParamValue");
50
+ function getViewStack(view) {
51
+ var _a;
52
+ const stack = (_a = view == null ? void 0 : view.navigation) == null ? void 0 : _a.viewStack;
53
+ return Array.isArray(stack) ? stack : [];
54
+ }
55
+ __name(getViewStack, "getViewStack");
56
+ function getAnchoredViewStackIndex(view, stack = getViewStack(view)) {
57
+ var _a;
58
+ if (!stack.length) return -1;
59
+ const args = (view == null ? void 0 : view.inputArgs) || {};
60
+ const navParams = ((_a = view == null ? void 0 : view.navigation) == null ? void 0 : _a.viewParams) || {};
61
+ const viewUid = args.viewUid ?? navParams.viewUid;
62
+ if (!viewUid) {
63
+ return stack.length - 1;
64
+ }
65
+ const candidates = stack.map((item, index) => ({ item, index })).filter(({ item }) => (item == null ? void 0 : item.viewUid) === viewUid);
66
+ if (!candidates.length) {
67
+ return stack.length - 1;
68
+ }
69
+ const keys = ["filterByTk", "sourceId", "tabUid"];
70
+ let bestIndex = candidates[candidates.length - 1].index;
71
+ let bestScore = -1;
72
+ for (const { item, index } of candidates) {
73
+ let score = 0;
74
+ let matched = true;
75
+ for (const key of keys) {
76
+ if (!isDefined(args[key])) continue;
77
+ if (!isSameViewParamValue(item == null ? void 0 : item[key], args[key])) {
78
+ matched = false;
79
+ break;
80
+ }
81
+ score += 1;
82
+ }
83
+ if (!matched) continue;
84
+ if (score >= bestScore) {
85
+ bestIndex = index;
86
+ bestScore = score;
87
+ }
88
+ }
89
+ return bestIndex;
90
+ }
91
+ __name(getAnchoredViewStackIndex, "getAnchoredViewStackIndex");
92
+ function getPopupView(ctx, anchorView) {
93
+ return anchorView ?? ctx.view;
94
+ }
95
+ __name(getPopupView, "getPopupView");
96
+ function isPopupView(view) {
97
+ var _a;
98
+ if (!view) return false;
99
+ const stack = getViewStack(view);
100
+ const openerUids = (_a = view == null ? void 0 : view.inputArgs) == null ? void 0 : _a.openerUids;
101
+ const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
102
+ return getAnchoredViewStackIndex(view, stack) >= 1 || hasOpener;
103
+ }
104
+ __name(isPopupView, "isPopupView");
36
105
  function isPlainObject(val) {
37
106
  if (val === null || typeof val !== "object") return false;
38
107
  const proto = Object.getPrototypeOf(val);
@@ -83,17 +152,9 @@ function makeMetaFromValue(value, title, seen) {
83
152
  __name(makeMetaFromValue, "makeMetaFromValue");
84
153
  function createPopupMeta(ctx, anchorView) {
85
154
  const t = /* @__PURE__ */ __name((k) => ctx.t(k), "t");
86
- const isPopupView = /* @__PURE__ */ __name((view) => {
87
- var _a, _b;
88
- if (!view) return false;
89
- const stack = Array.isArray((_a = view.navigation) == null ? void 0 : _a.viewStack) ? view.navigation.viewStack : [];
90
- const openerUids = (_b = view == null ? void 0 : view.inputArgs) == null ? void 0 : _b.openerUids;
91
- const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
92
- return stack.length >= 2 || hasOpener;
93
- }, "isPopupView");
94
- const hasPopupNow = /* @__PURE__ */ __name(() => isPopupView(anchorView ?? ctx.view), "hasPopupNow");
155
+ const hasPopupNow = /* @__PURE__ */ __name((flowCtx = ctx) => isPopupView(getPopupView(flowCtx, anchorView)), "hasPopupNow");
95
156
  const resolveRecordRef = /* @__PURE__ */ __name(async (flowCtx) => {
96
- const view = anchorView ?? flowCtx.view;
157
+ const view = getPopupView(flowCtx, anchorView);
97
158
  if (!view || !isPopupView(view)) return void 0;
98
159
  const base = await buildPopupRuntime(flowCtx, view);
99
160
  const res = base == null ? void 0 : base.resource;
@@ -116,25 +177,26 @@ function createPopupMeta(ctx, anchorView) {
116
177
  return ((_d = (_c = ds == null ? void 0 : ds.collectionManager) == null ? void 0 : _c.getCollection) == null ? void 0 : _d.call(_c, ref.collection)) || null;
117
178
  }, "getCurrentCollection");
118
179
  const getParentRecordRef = /* @__PURE__ */ __name(async (level, flowCtx) => {
119
- var _a, _b, _c, _d, _e, _f, _g;
180
+ var _a, _b, _c, _d, _e, _f;
120
181
  try {
121
182
  const useCtx = flowCtx || ctx;
122
- const nav = (_a = useCtx.view) == null ? void 0 : _a.navigation;
123
- const stack = Array.isArray(nav == null ? void 0 : nav.viewStack) ? nav.viewStack : [];
124
- if (stack.length < 2 || level < 1) return void 0;
125
- const idx = stack.length - 1 - level;
126
- if (idx < 0) return void 0;
183
+ const view = getPopupView(useCtx, anchorView);
184
+ const stack = getViewStack(view);
185
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
186
+ if (currentIndex < 1 || level < 1) return void 0;
187
+ const idx = currentIndex - level;
188
+ if (idx < 1) return void 0;
127
189
  const parent = stack[idx];
128
190
  if (!(parent == null ? void 0 : parent.viewUid)) return void 0;
129
- let model = (_b = useCtx.engine) == null ? void 0 : _b.getModel(parent.viewUid, true);
191
+ let model = (_a = useCtx.engine) == null ? void 0 : _a.getModel(parent.viewUid, true);
130
192
  if (!model) {
131
193
  try {
132
194
  model = await useCtx.engine.loadModel({ uid: parent.viewUid });
133
195
  } catch (e) {
134
- (_d = (_c = useCtx.logger || ctx.logger) == null ? void 0 : _c.warn) == null ? void 0 : _d.call(_c, { err: e }, "[FlowEngine] popup.getParentRecordRef loadModel failed");
196
+ (_c = (_b = useCtx.logger || ctx.logger) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, { err: e }, "[FlowEngine] popup.getParentRecordRef loadModel failed");
135
197
  }
136
198
  }
137
- const params = ((_e = model == null ? void 0 : model.getStepParams) == null ? void 0 : _e.call(model, "popupSettings", "openView")) || {};
199
+ const params = ((_d = model == null ? void 0 : model.getStepParams) == null ? void 0 : _d.call(model, "popupSettings", "openView")) || {};
138
200
  const collection = params == null ? void 0 : params.collectionName;
139
201
  const dataSourceKey = (params == null ? void 0 : params.dataSourceKey) || "main";
140
202
  const filterByTk = (parent == null ? void 0 : parent.filterByTk) ?? (parent == null ? void 0 : parent.sourceId);
@@ -148,16 +210,16 @@ function createPopupMeta(ctx, anchorView) {
148
210
  };
149
211
  return ref;
150
212
  } catch (e) {
151
- (_g = (_f = (flowCtx == null ? void 0 : flowCtx.logger) || ctx.logger) == null ? void 0 : _f.warn) == null ? void 0 : _g.call(_f, { err: e }, "[FlowEngine] popup.getParentRecordRef failed");
213
+ (_f = (_e = (flowCtx == null ? void 0 : flowCtx.logger) || ctx.logger) == null ? void 0 : _e.warn) == null ? void 0 : _f.call(_e, { err: e }, "[FlowEngine] popup.getParentRecordRef failed");
152
214
  return void 0;
153
215
  }
154
216
  }, "getParentRecordRef");
155
217
  const hasParentNow = /* @__PURE__ */ __name((level) => {
156
- var _a;
157
218
  try {
158
- const nav = (_a = anchorView ?? ctx.view) == null ? void 0 : _a.navigation;
159
- const stack = Array.isArray(nav == null ? void 0 : nav.viewStack) ? nav.viewStack : [];
160
- return stack.length >= level + 1;
219
+ const view = getPopupView(ctx, anchorView);
220
+ const stack = getViewStack(view);
221
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
222
+ return currentIndex - level >= 1;
161
223
  } catch (_) {
162
224
  return false;
163
225
  }
@@ -220,10 +282,11 @@ function createPopupMeta(ctx, anchorView) {
220
282
  disabled: /* @__PURE__ */ __name(() => !hasPopupNow(), "disabled"),
221
283
  hidden: /* @__PURE__ */ __name(() => !hasPopupNow(), "hidden"),
222
284
  buildVariablesParams: /* @__PURE__ */ __name(async (c) => {
223
- var _a, _b, _c, _d, _e, _f;
224
- if (!hasPopupNow()) return void 0;
285
+ var _a, _b, _c, _d;
286
+ if (!hasPopupNow(c)) return void 0;
225
287
  const ref = await resolveRecordRef(c);
226
- const inputArgs = (_a = c.view) == null ? void 0 : _a.inputArgs;
288
+ const view = getPopupView(c, anchorView);
289
+ const inputArgs = view == null ? void 0 : view.inputArgs;
227
290
  const params = {};
228
291
  if (ref) {
229
292
  const merged = { ...ref };
@@ -236,9 +299,9 @@ function createPopupMeta(ctx, anchorView) {
236
299
  params.record = merged;
237
300
  }
238
301
  try {
239
- const nav = (_b = c.view) == null ? void 0 : _b.navigation;
240
- const stack = Array.isArray(nav == null ? void 0 : nav.viewStack) ? nav.viewStack : [];
241
- if (stack.length >= 2) {
302
+ const stack = getViewStack(view);
303
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
304
+ if (currentIndex >= 2) {
242
305
  let cur = params;
243
306
  let level = 1;
244
307
  let parentRef = await getParentRecordRef(level, c);
@@ -251,7 +314,7 @@ function createPopupMeta(ctx, anchorView) {
251
314
  }
252
315
  }
253
316
  } catch (err) {
254
- (_d = (_c = c.logger) == null ? void 0 : _c.debug) == null ? void 0 : _d.call(_c, { err }, "[FlowEngine] buildVariablesParams: build parent-chain failed");
317
+ (_b = (_a = c.logger) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(_a, { err }, "[FlowEngine] buildVariablesParams: build parent-chain failed");
255
318
  }
256
319
  try {
257
320
  const srcId = inputArgs == null ? void 0 : inputArgs.sourceId;
@@ -268,12 +331,12 @@ function createPopupMeta(ctx, anchorView) {
268
331
  }
269
332
  }
270
333
  } catch (err) {
271
- (_f = (_e = c.logger) == null ? void 0 : _e.debug) == null ? void 0 : _f.call(_e, { err }, "[FlowEngine] buildVariablesParams: infer sourceRecord failed");
334
+ (_d = (_c = c.logger) == null ? void 0 : _c.debug) == null ? void 0 : _d.call(_c, { err }, "[FlowEngine] buildVariablesParams: infer sourceRecord failed");
272
335
  }
273
336
  return params;
274
337
  }, "buildVariablesParams"),
275
338
  properties: /* @__PURE__ */ __name(async () => {
276
- var _a, _b, _c, _d, _e, _f;
339
+ var _a, _b, _c, _d;
277
340
  const props = {};
278
341
  props.uid = { type: "string", title: t("Popup uid") };
279
342
  const recordRef = await resolveRecordRef(ctx);
@@ -292,20 +355,21 @@ function createPopupMeta(ctx, anchorView) {
292
355
  props.record = recordFactory;
293
356
  }
294
357
  try {
295
- const inputArgs = (_a = ctx.view) == null ? void 0 : _a.inputArgs;
358
+ const view = getPopupView(ctx, anchorView);
359
+ const inputArgs = view == null ? void 0 : view.inputArgs;
296
360
  const srcId = inputArgs == null ? void 0 : inputArgs.sourceId;
297
361
  let assoc = inputArgs == null ? void 0 : inputArgs.associationName;
298
362
  let dsKey = (inputArgs == null ? void 0 : inputArgs.dataSourceKey) || "main";
299
363
  if (!assoc || typeof assoc !== "string" || !assoc.includes(".")) {
300
- const nav = (_b = ctx.view) == null ? void 0 : _b.navigation;
301
- const stack = Array.isArray(nav == null ? void 0 : nav.viewStack) ? nav.viewStack : [];
302
- const last = stack == null ? void 0 : stack[stack.length - 1];
303
- if (last == null ? void 0 : last.viewUid) {
304
- let model = (_c = ctx == null ? void 0 : ctx.engine) == null ? void 0 : _c.getModel(last.viewUid, true);
364
+ const stack = getViewStack(view);
365
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
366
+ const current = currentIndex >= 0 ? stack == null ? void 0 : stack[currentIndex] : void 0;
367
+ if (current == null ? void 0 : current.viewUid) {
368
+ let model = (_a = ctx == null ? void 0 : ctx.engine) == null ? void 0 : _a.getModel(current.viewUid, true);
305
369
  if (!model) {
306
- model = await ctx.engine.loadModel({ uid: last.viewUid });
370
+ model = await ctx.engine.loadModel({ uid: current.viewUid });
307
371
  }
308
- const p = ((_d = model == null ? void 0 : model.getStepParams) == null ? void 0 : _d.call(model, "popupSettings", "openView")) || {};
372
+ const p = ((_b = model == null ? void 0 : model.getStepParams) == null ? void 0 : _b.call(model, "popupSettings", "openView")) || {};
309
373
  assoc = (p == null ? void 0 : p.associationName) || assoc;
310
374
  dsKey = (p == null ? void 0 : p.dataSourceKey) || dsKey;
311
375
  }
@@ -333,7 +397,7 @@ function createPopupMeta(ctx, anchorView) {
333
397
  }
334
398
  }
335
399
  } catch (err) {
336
- (_f = (_e = ctx.logger) == null ? void 0 : _e.debug) == null ? void 0 : _f.call(_e, { err }, "[FlowEngine] popup.properties: build sourceRecord failed");
400
+ (_d = (_c = ctx.logger) == null ? void 0 : _c.debug) == null ? void 0 : _d.call(_c, { err }, "[FlowEngine] popup.properties: build sourceRecord failed");
337
401
  }
338
402
  const resourceMeta = {
339
403
  type: "object",
@@ -362,12 +426,12 @@ function createPopupMeta(ctx, anchorView) {
362
426
  }
363
427
  __name(createPopupMeta, "createPopupMeta");
364
428
  async function buildPopupRuntime(ctx, view) {
365
- var _a, _b, _c;
366
- const nav = view == null ? void 0 : view.navigation;
367
- const stack = Array.isArray(nav == null ? void 0 : nav.viewStack) ? nav.viewStack : [];
429
+ var _a;
430
+ const stack = getViewStack(view);
431
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
368
432
  const openerUids = (_a = view == null ? void 0 : view.inputArgs) == null ? void 0 : _a.openerUids;
369
433
  const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
370
- const hasStackPopup = stack.length >= 2;
434
+ const hasStackPopup = currentIndex >= 1;
371
435
  const isPopup = hasStackPopup || hasOpener;
372
436
  if (!isPopup) return void 0;
373
437
  if (!stack.length) {
@@ -386,12 +450,12 @@ async function buildPopupRuntime(ctx, view) {
386
450
  };
387
451
  }
388
452
  const buildNode = /* @__PURE__ */ __name(async (idx) => {
389
- var _a2, _b2, _c2, _d, _e, _f;
453
+ var _a2, _b, _c, _d, _e, _f;
390
454
  if (idx < 0 || !((_a2 = stack[idx]) == null ? void 0 : _a2.viewUid)) return void 0;
391
455
  const viewUid = stack[idx].viewUid;
392
- let model = (_b2 = ctx.engine) == null ? void 0 : _b2.getModel(viewUid, true);
456
+ let model = (_b = ctx.engine) == null ? void 0 : _b.getModel(viewUid, true);
393
457
  if (!model) {
394
- model = await ((_c2 = ctx.engine) == null ? void 0 : _c2.loadModel({ uid: viewUid }));
458
+ model = await ((_c = ctx.engine) == null ? void 0 : _c.loadModel({ uid: viewUid }));
395
459
  }
396
460
  const p = ((_d = model == null ? void 0 : model.getStepParams) == null ? void 0 : _d.call(model, "popupSettings", "openView")) || {};
397
461
  const collectionName = p == null ? void 0 : p.collectionName;
@@ -410,7 +474,7 @@ async function buildPopupRuntime(ctx, view) {
410
474
  if (parentNode) node.parent = parentNode;
411
475
  return node;
412
476
  }, "buildNode");
413
- const currentNode = await buildNode((((_c = (_b = view == null ? void 0 : view.navigation) == null ? void 0 : _b.viewStack) == null ? void 0 : _c.length) || 1) - 1);
477
+ const currentNode = await buildNode(currentIndex);
414
478
  return currentNode;
415
479
  }
416
480
  __name(buildPopupRuntime, "buildPopupRuntime");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.57",
3
+ "version": "2.0.59",
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.0.57",
12
- "@nocobase/shared": "2.0.57",
11
+ "@nocobase/sdk": "2.0.59",
12
+ "@nocobase/shared": "2.0.59",
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": "629bf05e63bca0cbd60fffb87057d45bda5d9222"
40
+ "gitHead": "8b3a0cfadf6cb2da4a7e9213ef0d8d092d885c4a"
41
41
  }
@@ -11,7 +11,7 @@ import { describe, it, expect, vi } from 'vitest';
11
11
  import { FlowContext } from '../flowContext';
12
12
  import { FlowEngine } from '../flowEngine';
13
13
  import type { FlowView } from '../views/FlowView';
14
- import { createPopupMeta } from '../views/createViewMeta';
14
+ import { buildPopupRuntime, createPopupMeta } from '../views/createViewMeta';
15
15
 
16
16
  describe('createPopupMeta - popup variables', () => {
17
17
  function makeCtx() {
@@ -23,6 +23,62 @@ describe('createPopupMeta - popup variables', () => {
23
23
  return { engine, ctx };
24
24
  }
25
25
 
26
+ function makeNestedPopupView(viewUid: string, filterByTk: number): FlowView {
27
+ return {
28
+ type: 'drawer',
29
+ inputArgs: {
30
+ viewUid,
31
+ filterByTk,
32
+ sourceId: 13,
33
+ },
34
+ Header: null,
35
+ Footer: null,
36
+ close: () => void 0,
37
+ update: () => void 0,
38
+ navigation: {
39
+ viewStack: [
40
+ { viewUid: 'base-page-uid' },
41
+ { viewUid: 'parent-popup-uid', filterByTk: 13, sourceId: 13 },
42
+ { viewUid: 'child-popup-uid', filterByTk: 24, sourceId: 13 },
43
+ ],
44
+ } as any,
45
+ } as any;
46
+ }
47
+
48
+ function mockNestedPopupModels(engine: FlowEngine) {
49
+ vi.spyOn(engine as any, 'getModel').mockImplementation((uid: string) => {
50
+ if (uid === 'parent-popup-uid') {
51
+ return {
52
+ getStepParams: vi.fn((_fk: string, sk: string) =>
53
+ sk === 'openView'
54
+ ? {
55
+ collectionName: 'users',
56
+ dataSourceKey: 'main',
57
+ associationName: 'users.orgs',
58
+ }
59
+ : undefined,
60
+ ),
61
+ };
62
+ }
63
+ if (uid === 'child-popup-uid') {
64
+ return {
65
+ getStepParams: vi.fn((_fk: string, sk: string) =>
66
+ sk === 'openView'
67
+ ? {
68
+ collectionName: 'orgs',
69
+ dataSourceKey: 'main',
70
+ associationName: 'users.orgs',
71
+ }
72
+ : undefined,
73
+ ),
74
+ };
75
+ }
76
+ return {
77
+ getStepParams: vi.fn(() => undefined),
78
+ };
79
+ });
80
+ }
81
+
26
82
  it('buildVariablesParams(record) uses anchor view instead of ctx.view', async () => {
27
83
  const { engine, ctx } = makeCtx();
28
84
 
@@ -108,6 +164,64 @@ describe('createPopupMeta - popup variables', () => {
108
164
  expect(vars.record.collection).not.toBe('comments');
109
165
  });
110
166
 
167
+ it('buildPopupRuntime anchors current popup even when the navigation stack already has a child popup', async () => {
168
+ const { engine, ctx } = makeCtx();
169
+ const parentView = makeNestedPopupView('parent-popup-uid', 13);
170
+ mockNestedPopupModels(engine);
171
+
172
+ const popup = await buildPopupRuntime(ctx, parentView);
173
+
174
+ expect(popup?.uid).toBe('parent-popup-uid');
175
+ expect(popup?.resource).toEqual({
176
+ dataSourceKey: 'main',
177
+ collectionName: 'users',
178
+ associationName: 'users.orgs',
179
+ filterByTk: 13,
180
+ sourceId: 13,
181
+ });
182
+ });
183
+
184
+ it('buildVariablesParams(record) keeps the parent view record when a child popup is open', async () => {
185
+ const { engine, ctx } = makeCtx();
186
+ const parentView = makeNestedPopupView('parent-popup-uid', 13);
187
+ mockNestedPopupModels(engine);
188
+
189
+ const meta = (await createPopupMeta(ctx, parentView)())!;
190
+ const vars = (await meta.buildVariablesParams!(ctx)) as any;
191
+
192
+ expect(vars.record).toEqual({
193
+ collection: 'users',
194
+ dataSourceKey: 'main',
195
+ filterByTk: 13,
196
+ associationName: 'users.orgs',
197
+ sourceId: 13,
198
+ });
199
+ });
200
+
201
+ it('buildVariablesParams(parent.record) is still relative to the child popup view', async () => {
202
+ const { engine, ctx } = makeCtx();
203
+ const childView = makeNestedPopupView('child-popup-uid', 24);
204
+ mockNestedPopupModels(engine);
205
+
206
+ const meta = (await createPopupMeta(ctx, childView)())!;
207
+ const vars = (await meta.buildVariablesParams!(ctx)) as any;
208
+
209
+ expect(vars.record).toEqual({
210
+ collection: 'orgs',
211
+ dataSourceKey: 'main',
212
+ filterByTk: 24,
213
+ associationName: 'users.orgs',
214
+ sourceId: 13,
215
+ });
216
+ expect(vars.parent.record).toEqual({
217
+ collection: 'users',
218
+ dataSourceKey: 'main',
219
+ filterByTk: 13,
220
+ sourceId: 13,
221
+ associationName: 'users.orgs',
222
+ });
223
+ });
224
+
111
225
  it('properties() provides a record factory node (lazy) with title', async () => {
112
226
  const { engine, ctx } = makeCtx();
113
227
  // 只要能通过 anchorView 推断到集合名和主键即可;集合详情在懒加载时再取
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { beforeEach, describe, expect, it } from 'vitest';
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
11
11
  import { FlowEngine } from '../flowEngine';
12
12
  import { FlowModel } from '../models';
13
13
 
@@ -20,6 +20,7 @@ describe('FlowEngine removeModel', () => {
20
20
  });
21
21
 
22
22
  it('removeModel should remove model but keep sub-models in cache (current behavior)', () => {
23
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
23
24
  const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
24
25
  const child = engine.createModel({
25
26
  uid: 'child',
@@ -32,14 +33,53 @@ describe('FlowEngine removeModel', () => {
32
33
  expect(engine.getModel('parent')).toBe(parent);
33
34
  expect(engine.getModel('child')).toBe(child);
34
35
 
35
- engine.removeModel('parent');
36
+ try {
37
+ engine.removeModel('parent');
38
+ } finally {
39
+ loggerSpy.mockRestore();
40
+ }
36
41
 
37
42
  expect(engine.getModel('parent')).toBeUndefined();
38
43
  // Current behavior: child is still in cache
39
44
  expect(engine.getModel('child')).toBeDefined();
40
45
  });
41
46
 
47
+ it('removeModel should log missing models at debug level', () => {
48
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
49
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
50
+
51
+ try {
52
+ expect(engine.removeModel('missing')).toBe(false);
53
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
54
+ expect(loggerSpy).toHaveBeenCalledWith("FlowEngine: Model with UID 'missing' does not exist.");
55
+ } finally {
56
+ consoleWarnSpy.mockRestore();
57
+ loggerSpy.mockRestore();
58
+ }
59
+ });
60
+
61
+ it('should reduce default logger verbosity in production', () => {
62
+ const originalNodeEnv = process.env.NODE_ENV;
63
+
64
+ try {
65
+ process.env.NODE_ENV = 'production';
66
+
67
+ const productionEngine = new FlowEngine();
68
+
69
+ expect(productionEngine.logger.level).toBe('warn');
70
+ expect(productionEngine.logger.isLevelEnabled('debug')).toBe(false);
71
+ expect(productionEngine.logger.isLevelEnabled('warn')).toBe(true);
72
+ } finally {
73
+ if (originalNodeEnv === undefined) {
74
+ delete process.env.NODE_ENV;
75
+ } else {
76
+ process.env.NODE_ENV = originalNodeEnv;
77
+ }
78
+ }
79
+ });
80
+
42
81
  it('removeModelWithSubModels should remove model and all sub-models from cache', () => {
82
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
43
83
  const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
44
84
  const child = engine.createModel({
45
85
  uid: 'child',
@@ -63,7 +103,11 @@ describe('FlowEngine removeModel', () => {
63
103
  expect(engine.getModel('child')).toBe(child);
64
104
  expect(engine.getModel('grandChild')).toBe(grandChild);
65
105
 
66
- engine.removeModelWithSubModels('parent');
106
+ try {
107
+ engine.removeModelWithSubModels('parent');
108
+ } finally {
109
+ loggerSpy.mockRestore();
110
+ }
67
111
 
68
112
  expect(engine.getModel('parent')).toBeUndefined();
69
113
  expect(engine.getModel('child')).toBeUndefined();
@@ -158,7 +158,7 @@ export class FlowExecutor {
158
158
  const stepDefaultParams = await resolveDefaultParams(step.defaultParams, runtimeCtx);
159
159
  combinedParams = { ...stepDefaultParams };
160
160
  } else {
161
- flowContext.logger.error(
161
+ flowContext.logger.warn(
162
162
  `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`,
163
163
  );
164
164
  continue;
@@ -81,6 +81,34 @@ describe('FlowExecutor', () => {
81
81
  expect(result.step2).toBe('step2-ok');
82
82
  });
83
83
 
84
+ it('runFlow warns and skips steps without use or handler', async () => {
85
+ const flows = {
86
+ referenceSettings: {
87
+ steps: {
88
+ target: {},
89
+ },
90
+ },
91
+ } satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
92
+ const model = createModelWithFlows('m-empty-step', flows);
93
+ const loggerChildSpy = vi.spyOn(engine.logger, 'child').mockReturnValue(engine.logger);
94
+ const loggerWarnSpy = vi.spyOn(engine.logger, 'warn').mockImplementation(() => {});
95
+ const loggerErrorSpy = vi.spyOn(engine.logger, 'error').mockImplementation(() => {});
96
+
97
+ try {
98
+ const result = await engine.executor.runFlow(model, 'referenceSettings');
99
+
100
+ expect(result).toEqual({});
101
+ expect(loggerWarnSpy).toHaveBeenCalledWith(
102
+ "BaseModel.applyFlow: Step 'target' in flow 'referenceSettings' has neither 'use' nor 'handler'. Skipping.",
103
+ );
104
+ expect(loggerErrorSpy).not.toHaveBeenCalled();
105
+ } finally {
106
+ loggerChildSpy.mockRestore();
107
+ loggerWarnSpy.mockRestore();
108
+ loggerErrorSpy.mockRestore();
109
+ }
110
+ });
111
+
84
112
  it("dispatchEvent('beforeRender') executes flows in sort order and caches result (when options specify)", async () => {
85
113
  const calls: string[] = [];
86
114
  const mkFlow = (key: string, sort: number) => ({
package/src/flowEngine.ts CHANGED
@@ -35,6 +35,8 @@ import type {
35
35
  } from './types';
36
36
  import { isInheritedFrom } from './utils';
37
37
 
38
+ const getFlowEngineLoggerLevel = () => (process.env.NODE_ENV === 'production' ? 'warn' : 'trace');
39
+
38
40
  /**
39
41
  * FlowEngine is the core class of the flow engine, responsible for managing flow models, actions, model repository, and more.
40
42
  * It provides capabilities for registering, creating, finding, persisting, replacing, and moving models.
@@ -183,7 +185,7 @@ export class FlowEngine {
183
185
  MultiRecordResource,
184
186
  });
185
187
  this.logger = pino({
186
- level: 'trace',
188
+ level: getFlowEngineLoggerLevel(),
187
189
  browser: {
188
190
  write: {
189
191
  fatal: (o) => console.trace(o),
@@ -608,7 +610,6 @@ export class FlowEngine {
608
610
 
609
611
  while (current) {
610
612
  if (visited.has(current)) {
611
- console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
612
613
  break;
613
614
  }
614
615
  visited.add(current);
@@ -727,7 +728,7 @@ export class FlowEngine {
727
728
  */
728
729
  public removeModel(uid: string): boolean {
729
730
  if (!this._modelInstances.has(uid)) {
730
- console.warn(`FlowEngine: Model with UID '${uid}' does not exist.`);
731
+ this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
731
732
  return false;
732
733
  }
733
734
  const modelInstance = this._modelInstances.get(uid) as FlowModel;
@@ -61,21 +61,6 @@ describe('FlowEngine.createModel resolveUse hook', () => {
61
61
  expect(warnSpy).not.toHaveBeenCalled();
62
62
  });
63
63
 
64
- test('should break resolveUse on circular reference and warn', () => {
65
- class LoopModel extends FlowModel {
66
- static resolveUse() {
67
- return 'LoopModel';
68
- }
69
- }
70
-
71
- engine.registerModels({ LoopModel });
72
-
73
- const model = engine.createModel({ use: 'LoopModel', uid: 'loop-model', flowEngine: engine });
74
-
75
- expect(model).toBeInstanceOf(LoopModel);
76
- expect(warnSpy).toHaveBeenCalled();
77
- });
78
-
79
64
  test('should fall back to ErrorFlowModel when resolveUse returns unregistered name', () => {
80
65
  class MissingTargetEntry extends FlowModel {
81
66
  static resolveUse() {
@@ -370,15 +370,17 @@ describe('FlowModel', () => {
370
370
  };
371
371
 
372
372
  TestFlowModel.registerFlow(exitFlow);
373
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
373
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
374
374
 
375
- const result = await model.applyFlow('exitFlow');
376
-
377
- expect(result).toBeInstanceOf(FlowExitAllException);
378
- expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
379
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
375
+ try {
376
+ const result = await model.applyFlow('exitFlow');
380
377
 
381
- consoleSpy.mockRestore();
378
+ expect(result).toBeInstanceOf(FlowExitAllException);
379
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
380
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
381
+ } finally {
382
+ loggerSpy.mockRestore();
383
+ }
382
384
  });
383
385
 
384
386
  test('should handle ctx.exit() as FlowExitAllException in beforeRender dispatch', async () => {
@@ -474,15 +476,17 @@ describe('FlowModel', () => {
474
476
  };
475
477
 
476
478
  TestFlowModel.registerFlow(exitFlow);
477
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
478
-
479
- const result = await model.applyFlow('exitFlow');
479
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
480
480
 
481
- expect(result).toBeInstanceOf(FlowExitAllException);
482
- expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
483
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
481
+ try {
482
+ const result = await model.applyFlow('exitFlow');
484
483
 
485
- consoleSpy.mockRestore();
484
+ expect(result).toBeInstanceOf(FlowExitAllException);
485
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
486
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
487
+ } finally {
488
+ loggerSpy.mockRestore();
489
+ }
486
490
  });
487
491
 
488
492
  test('should propagate step execution errors', async () => {
@@ -768,7 +772,7 @@ describe('FlowModel', () => {
768
772
  const eventFlow = createEventFlowDefinition('testEvent');
769
773
  TestFlowModel.registerFlow(eventFlow);
770
774
 
771
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
775
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
772
776
 
773
777
  try {
774
778
  model.dispatchEvent('testEvent', { data: 'payload' });
@@ -776,7 +780,7 @@ describe('FlowModel', () => {
776
780
  // Use a more reliable approach than arbitrary timeout
777
781
  await new Promise((resolve) => setTimeout(resolve, 0));
778
782
 
779
- expect(consoleSpy).toHaveBeenCalledWith(
783
+ expect(loggerSpy).toHaveBeenCalledWith(
780
784
  expect.stringContaining('[FlowModel] dispatchEvent: uid=test-model-uid, event=testEvent'),
781
785
  );
782
786
  expect(eventFlow.steps.eventStep.handler).toHaveBeenCalledWith(
@@ -786,7 +790,7 @@ describe('FlowModel', () => {
786
790
  expect.any(Object),
787
791
  );
788
792
  } finally {
789
- consoleSpy.mockRestore();
793
+ loggerSpy.mockRestore();
790
794
  }
791
795
  });
792
796
 
@@ -1597,7 +1601,7 @@ describe('FlowModel', () => {
1597
1601
  fork1.dispose = vi.fn();
1598
1602
  fork2.dispose = vi.fn();
1599
1603
 
1600
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1604
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1601
1605
 
1602
1606
  try {
1603
1607
  model.clearForks();
@@ -1606,19 +1610,19 @@ describe('FlowModel', () => {
1606
1610
  expect(fork2.dispose).toHaveBeenCalled();
1607
1611
  expect(model.forks.size).toBe(0);
1608
1612
  } finally {
1609
- consoleSpy.mockRestore();
1613
+ loggerSpy.mockRestore();
1610
1614
  }
1611
1615
  });
1612
1616
 
1613
1617
  test('should handle empty forks collection when clearing', () => {
1614
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1618
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1615
1619
 
1616
1620
  try {
1617
1621
  model.clearForks();
1618
1622
 
1619
1623
  expect(model.forks.size).toBe(0);
1620
1624
  } finally {
1621
- consoleSpy.mockRestore();
1625
+ loggerSpy.mockRestore();
1622
1626
  }
1623
1627
  });
1624
1628
  });
@@ -1746,7 +1750,7 @@ describe('FlowModel', () => {
1746
1750
  test('should clean up resources on remove', () => {
1747
1751
  model.createFork();
1748
1752
  model.createFork();
1749
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1753
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1750
1754
 
1751
1755
  // Mock removeModel to simulate proper fork cleanup
1752
1756
  flowEngine.removeModel = vi.fn().mockImplementation(() => {
@@ -1763,7 +1767,7 @@ describe('FlowModel', () => {
1763
1767
  expect(model.forks.size).toBe(0);
1764
1768
  expect(flowEngine.removeModel).toHaveBeenCalledWith(model.uid);
1765
1769
  } finally {
1766
- consoleSpy.mockRestore();
1770
+ loggerSpy.mockRestore();
1767
1771
  }
1768
1772
  });
1769
1773
  });
@@ -1840,17 +1844,12 @@ describe('FlowModel', () => {
1840
1844
  });
1841
1845
 
1842
1846
  test('should rerender triggers beforeRender without cache', async () => {
1843
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1844
1847
  model.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
1845
1848
 
1846
- try {
1847
- await expect(model.rerender()).resolves.not.toThrow();
1848
- expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
1849
- useCache: false,
1850
- });
1851
- } finally {
1852
- consoleSpy.mockRestore();
1853
- }
1849
+ await expect(model.rerender()).resolves.not.toThrow();
1850
+ expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
1851
+ useCache: false,
1852
+ });
1854
1853
  });
1855
1854
  });
1856
1855
 
@@ -2874,7 +2873,7 @@ describe('FlowModel', () => {
2874
2873
  describe('Edge Cases & Error Handling', () => {
2875
2874
  test('should handle model destruction gracefully', () => {
2876
2875
  const model = new FlowModel(modelOptions);
2877
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
2876
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
2878
2877
 
2879
2878
  model.createFork();
2880
2879
  model.setProps({ testProp: 'value' });
@@ -2882,7 +2881,7 @@ describe('FlowModel', () => {
2882
2881
  try {
2883
2882
  expect(() => model.remove()).not.toThrow();
2884
2883
  } finally {
2885
- consoleSpy.mockRestore();
2884
+ loggerSpy.mockRestore();
2886
2885
  }
2887
2886
  });
2888
2887
 
@@ -757,7 +757,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
757
757
  }
758
758
  const isFork = (this as any).isFork === true;
759
759
  const target = this;
760
- console.log(
760
+ currentFlowEngine.logger.debug(
761
761
  `[FlowModel] applyFlow: uid=${this.uid}, flowKey=${flowKey}, isFork=${isFork}, cleanRun=${
762
762
  this.cleanRun
763
763
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -777,7 +777,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
777
777
  }
778
778
  const isFork = (this as any).isFork === true;
779
779
  const target = this;
780
- console.log(
780
+ currentFlowEngine.logger.debug(
781
781
  `[FlowModel] dispatchEvent: uid=${this.uid}, event=${eventName}, isFork=${isFork}, cleanRun=${
782
782
  this.cleanRun
783
783
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -1313,7 +1313,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1313
1313
  }
1314
1314
 
1315
1315
  clearForks() {
1316
- console.log(`FlowModel ${this.uid} clearing all forks.`);
1316
+ this.flowEngine.logger.debug(`FlowModel ${this.uid} clearing all forks.`);
1317
1317
  // 主动使所有 fork 失效
1318
1318
  if (this.forks?.size) {
1319
1319
  this.forks.forEach((fork) => fork.dispose());
@@ -15,6 +15,82 @@ import type { FlowView } from './FlowView';
15
15
 
16
16
  type PopupModelLike = { getStepParams?: (a: string, b: string) => any } | undefined;
17
17
 
18
+ function isDefined(value: any) {
19
+ return value !== undefined && value !== null;
20
+ }
21
+
22
+ function isSameViewParamValue(left: any, right: any) {
23
+ if (left === right) return true;
24
+ if (!isDefined(left) || !isDefined(right)) return false;
25
+
26
+ try {
27
+ return JSON.stringify(left) === JSON.stringify(right);
28
+ } catch (_) {
29
+ return String(left) === String(right);
30
+ }
31
+ }
32
+
33
+ function getViewStack(view?: FlowView): any[] {
34
+ const stack = view?.navigation?.viewStack;
35
+ return Array.isArray(stack) ? stack : [];
36
+ }
37
+
38
+ function getAnchoredViewStackIndex(view?: FlowView, stack = getViewStack(view)): number {
39
+ if (!stack.length) return -1;
40
+
41
+ const args = view?.inputArgs || {};
42
+ const navParams = view?.navigation?.viewParams || {};
43
+ const viewUid = args.viewUid ?? navParams.viewUid;
44
+
45
+ if (!viewUid) {
46
+ return stack.length - 1;
47
+ }
48
+
49
+ const candidates = stack.map((item, index) => ({ item, index })).filter(({ item }) => item?.viewUid === viewUid);
50
+
51
+ if (!candidates.length) {
52
+ return stack.length - 1;
53
+ }
54
+
55
+ const keys = ['filterByTk', 'sourceId', 'tabUid'];
56
+ let bestIndex = candidates[candidates.length - 1].index;
57
+ let bestScore = -1;
58
+
59
+ for (const { item, index } of candidates) {
60
+ let score = 0;
61
+ let matched = true;
62
+
63
+ for (const key of keys) {
64
+ if (!isDefined(args[key])) continue;
65
+ if (!isSameViewParamValue(item?.[key], args[key])) {
66
+ matched = false;
67
+ break;
68
+ }
69
+ score += 1;
70
+ }
71
+
72
+ if (!matched) continue;
73
+ if (score >= bestScore) {
74
+ bestIndex = index;
75
+ bestScore = score;
76
+ }
77
+ }
78
+
79
+ return bestIndex;
80
+ }
81
+
82
+ function getPopupView(ctx: FlowContext, anchorView?: FlowView) {
83
+ return anchorView ?? ctx.view;
84
+ }
85
+
86
+ function isPopupView(view?: FlowView): boolean {
87
+ if (!view) return false;
88
+ const stack = getViewStack(view);
89
+ const openerUids = view?.inputArgs?.openerUids;
90
+ const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
91
+ return getAnchoredViewStackIndex(view, stack) >= 1 || hasOpener;
92
+ }
93
+
18
94
  // 判断是否为普通对象(Plain Object),避免对类实例/代理等进行深度遍历
19
95
  function isPlainObject(val: any) {
20
96
  if (val === null || typeof val !== 'object') return false;
@@ -79,19 +155,11 @@ function makeMetaFromValue(value: any, title?: string, seen?: WeakSet<any>): any
79
155
  export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): PropertyMetaFactory {
80
156
  const t = (k: string) => ctx.t(k);
81
157
 
82
- const isPopupView = (view?: FlowView): boolean => {
83
- if (!view) return false;
84
- const stack = Array.isArray(view.navigation?.viewStack) ? view.navigation.viewStack : [];
85
- const openerUids = view?.inputArgs?.openerUids;
86
- const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
87
- return stack.length >= 2 || hasOpener;
88
- };
89
-
90
- const hasPopupNow = (): boolean => isPopupView(anchorView ?? ctx.view);
158
+ const hasPopupNow = (flowCtx: FlowContext = ctx): boolean => isPopupView(getPopupView(flowCtx, anchorView));
91
159
 
92
160
  // 统一解析锚定视图下的 RecordRef,避免在设置弹窗等二级视图中被误导
93
161
  const resolveRecordRef = async (flowCtx: FlowContext): Promise<RecordRef | undefined> => {
94
- const view = anchorView ?? flowCtx.view;
162
+ const view = getPopupView(flowCtx, anchorView);
95
163
  if (!view || !isPopupView(view)) return undefined;
96
164
 
97
165
  const base = await buildPopupRuntime(flowCtx, view);
@@ -119,11 +187,12 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
119
187
  const getParentRecordRef = async (level: number, flowCtx?: FlowContext): Promise<RecordRef | undefined> => {
120
188
  try {
121
189
  const useCtx = flowCtx || ctx;
122
- const nav = useCtx.view?.navigation;
123
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
124
- if (stack.length < 2 || level < 1) return undefined;
125
- const idx = stack.length - 1 - level;
126
- if (idx < 0) return undefined;
190
+ const view = getPopupView(useCtx, anchorView);
191
+ const stack = getViewStack(view);
192
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
193
+ if (currentIndex < 1 || level < 1) return undefined;
194
+ const idx = currentIndex - level;
195
+ if (idx < 1) return undefined;
127
196
  const parent = stack[idx];
128
197
  if (!parent?.viewUid) return undefined;
129
198
 
@@ -156,9 +225,10 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
156
225
 
157
226
  const hasParentNow = (level: number): boolean => {
158
227
  try {
159
- const nav = (anchorView ?? ctx.view)?.navigation;
160
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
161
- return stack.length >= level + 1; // level=1 需要至少2层
228
+ const view = getPopupView(ctx, anchorView);
229
+ const stack = getViewStack(view);
230
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
231
+ return currentIndex - level >= 1; // level=1 需要至少一个上级弹窗
162
232
  } catch (_) {
163
233
  return false;
164
234
  }
@@ -231,9 +301,10 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
231
301
  disabled: () => !hasPopupNow(),
232
302
  hidden: () => !hasPopupNow(),
233
303
  buildVariablesParams: async (c) => {
234
- if (!hasPopupNow()) return undefined;
304
+ if (!hasPopupNow(c)) return undefined;
235
305
  const ref = await resolveRecordRef(c);
236
- const inputArgs = c.view?.inputArgs;
306
+ const view = getPopupView(c, anchorView);
307
+ const inputArgs = view?.inputArgs;
237
308
  type PopupVariableParams = {
238
309
  record?: RecordRef;
239
310
  sourceRecord?: RecordRef;
@@ -253,9 +324,9 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
253
324
 
254
325
  // 构建 parent 链(用于服务端解析 ctx.popup.parent[.parent...].record.*)
255
326
  try {
256
- const nav = c.view?.navigation;
257
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
258
- if (stack.length >= 2) {
327
+ const stack = getViewStack(view);
328
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
329
+ if (currentIndex >= 2) {
259
330
  let cur: Record<string, any> = params;
260
331
  let level = 1;
261
332
  let parentRef = await getParentRecordRef(level, c);
@@ -315,20 +386,21 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
315
386
  }
316
387
  // 当 view.inputArgs 带有 sourceId + associationName 时,提供“上级记录”变量(基于 sourceId 推断)
317
388
  try {
318
- const inputArgs = ctx.view?.inputArgs;
389
+ const view = getPopupView(ctx, anchorView);
390
+ const inputArgs = view?.inputArgs;
319
391
  const srcId = inputArgs?.sourceId;
320
392
  let assoc: string | undefined = inputArgs?.associationName;
321
393
  let dsKey: string = inputArgs?.dataSourceKey || 'main';
322
394
 
323
395
  // 兜底:若 associationName 缺失或不含“.”,尝试从当前视图模型的 openView 参数推断
324
396
  if (!assoc || typeof assoc !== 'string' || !assoc.includes('.')) {
325
- const nav = ctx.view?.navigation;
326
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
327
- const last = stack?.[stack.length - 1];
328
- if (last?.viewUid) {
329
- let model = ctx?.engine?.getModel(last.viewUid, true) as PopupModelLike;
397
+ const stack = getViewStack(view);
398
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
399
+ const current = currentIndex >= 0 ? stack?.[currentIndex] : undefined;
400
+ if (current?.viewUid) {
401
+ let model = ctx?.engine?.getModel(current.viewUid, true) as PopupModelLike;
330
402
  if (!model) {
331
- model = (await ctx.engine.loadModel({ uid: last.viewUid })) as PopupModelLike;
403
+ model = (await ctx.engine.loadModel({ uid: current.viewUid })) as PopupModelLike;
332
404
  }
333
405
  const p = model?.getStepParams?.('popupSettings', 'openView') || {};
334
406
  assoc = p?.associationName || assoc;
@@ -406,12 +478,12 @@ interface PopupNode {
406
478
  }
407
479
 
408
480
  export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined> {
409
- const nav = view?.navigation;
410
- const stack = Array.isArray(nav?.viewStack) ? nav.viewStack : [];
481
+ const stack = getViewStack(view);
482
+ const currentIndex = getAnchoredViewStackIndex(view, stack);
411
483
 
412
484
  const openerUids = view?.inputArgs?.openerUids;
413
485
  const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
414
- const hasStackPopup = stack.length >= 2;
486
+ const hasStackPopup = currentIndex >= 1;
415
487
  const isPopup = hasStackPopup || hasOpener;
416
488
  if (!isPopup) return undefined;
417
489
 
@@ -457,7 +529,7 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
457
529
  if (parentNode) node.parent = parentNode;
458
530
  return node;
459
531
  };
460
- const currentNode = await buildNode((view?.navigation?.viewStack?.length || 1) - 1);
532
+ const currentNode = await buildNode(currentIndex);
461
533
  return currentNode;
462
534
  }
463
535