@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.
- package/lib/executor/FlowExecutor.js +1 -1
- package/lib/flowEngine.js +3 -3
- package/lib/models/flowModel.js +3 -3
- package/lib/views/createViewMeta.js +114 -50
- package/package.json +4 -4
- package/src/__tests__/createViewMeta.popup.test.ts +115 -1
- package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
- package/src/executor/FlowExecutor.ts +1 -1
- package/src/executor/__tests__/flowExecutor.test.ts +28 -0
- package/src/flowEngine.ts +4 -3
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +33 -34
- package/src/models/flowModel.tsx +3 -3
- package/src/views/createViewMeta.ts +106 -34
|
@@ -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.
|
|
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:
|
|
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
|
-
|
|
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);
|
package/lib/models/flowModel.js
CHANGED
|
@@ -624,7 +624,7 @@ const _FlowModel = class _FlowModel {
|
|
|
624
624
|
}
|
|
625
625
|
const isFork = this.isFork === true;
|
|
626
626
|
const target = this;
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
180
|
+
var _a, _b, _c, _d, _e, _f;
|
|
120
181
|
try {
|
|
121
182
|
const useCtx = flowCtx || ctx;
|
|
122
|
-
const
|
|
123
|
-
const stack =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 = (
|
|
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
|
-
(
|
|
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 = ((
|
|
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
|
-
(
|
|
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
|
|
159
|
-
const stack =
|
|
160
|
-
|
|
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
|
|
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
|
|
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
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
let model = (
|
|
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:
|
|
370
|
+
model = await ctx.engine.loadModel({ uid: current.viewUid });
|
|
307
371
|
}
|
|
308
|
-
const p = ((
|
|
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
|
-
(
|
|
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
|
|
366
|
-
const
|
|
367
|
-
const
|
|
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 =
|
|
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,
|
|
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 = (
|
|
456
|
+
let model = (_b = ctx.engine) == null ? void 0 : _b.getModel(viewUid, true);
|
|
393
457
|
if (!model) {
|
|
394
|
-
model = await ((
|
|
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(
|
|
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.
|
|
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.
|
|
12
|
-
"@nocobase/shared": "2.0.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
373
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
374
374
|
|
|
375
|
-
|
|
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
|
-
|
|
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
|
|
478
|
-
|
|
479
|
-
const result = await model.applyFlow('exitFlow');
|
|
479
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
480
480
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
|
|
481
|
+
try {
|
|
482
|
+
const result = await model.applyFlow('exitFlow');
|
|
484
483
|
|
|
485
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1613
|
+
loggerSpy.mockRestore();
|
|
1610
1614
|
}
|
|
1611
1615
|
});
|
|
1612
1616
|
|
|
1613
1617
|
test('should handle empty forks collection when clearing', () => {
|
|
1614
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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
|
|
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
|
-
|
|
2884
|
+
loggerSpy.mockRestore();
|
|
2886
2885
|
}
|
|
2887
2886
|
});
|
|
2888
2887
|
|
package/src/models/flowModel.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
123
|
-
const stack =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
160
|
-
const stack =
|
|
161
|
-
|
|
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
|
|
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
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
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
|
|
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
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
329
|
-
let model = ctx?.engine?.getModel(
|
|
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:
|
|
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
|
|
410
|
-
const
|
|
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 =
|
|
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(
|
|
532
|
+
const currentNode = await buildNode(currentIndex);
|
|
461
533
|
return currentNode;
|
|
462
534
|
}
|
|
463
535
|
|