@nocobase/flow-engine 2.0.0-alpha.45 → 2.0.0-alpha.46
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/components/FlowModelRenderer.js +3 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +16 -3
- package/lib/flowEngine.d.ts +5 -0
- package/lib/flowEngine.js +40 -1
- package/lib/flowSettings.js +2 -0
- package/lib/models/flowModel.d.ts +6 -1
- package/package.json +4 -4
- package/src/components/FlowModelRenderer.tsx +4 -2
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +17 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +55 -0
- package/src/flowEngine.ts +54 -1
- package/src/flowSettings.ts +4 -0
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +112 -0
- package/src/models/flowModel.tsx +10 -0
|
@@ -112,7 +112,9 @@ const FlowModelRendererCore = (0, import_reactive_react.observer)(
|
|
|
112
112
|
}
|
|
113
113
|
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, rendered);
|
|
114
114
|
}, "ContentOrError");
|
|
115
|
-
const
|
|
115
|
+
const rawUse = model == null ? void 0 : model.use;
|
|
116
|
+
const resolvedName = ((_a = model == null ? void 0 : model.constructor) == null ? void 0 : _a.name) || model.uid;
|
|
117
|
+
const contentKey = typeof rawUse === "string" ? `${rawUse}:${model.uid}` : `${resolvedName}:${model.uid}`;
|
|
116
118
|
if (!showFlowSettings) {
|
|
117
119
|
return wrapWithErrorBoundary(
|
|
118
120
|
/* @__PURE__ */ import_react.default.createElement("div", { key: contentKey }, /* @__PURE__ */ import_react.default.createElement(ContentOrError, null))
|
|
@@ -227,14 +227,27 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
|
|
|
227
227
|
);
|
|
228
228
|
const getModelConfigurableFlowsAndSteps = (0, import_react.useCallback)(
|
|
229
229
|
async (targetModel, modelKey) => {
|
|
230
|
+
var _a;
|
|
230
231
|
try {
|
|
231
|
-
const
|
|
232
|
+
const flowsMap = new Map(targetModel.constructor.globalFlowRegistry.getFlows());
|
|
233
|
+
const originUse = targetModel == null ? void 0 : targetModel.use;
|
|
234
|
+
if (typeof originUse === "string" && originUse !== targetModel.constructor.name) {
|
|
235
|
+
const originCls = (_a = targetModel.flowEngine) == null ? void 0 : _a.getModelClass(originUse);
|
|
236
|
+
if (originCls == null ? void 0 : originCls.globalFlowRegistry) {
|
|
237
|
+
for (const [k, v] of originCls.globalFlowRegistry.getFlows()) {
|
|
238
|
+
if (!flowsMap.has(k)) {
|
|
239
|
+
flowsMap.set(k, v);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const flows = flowsMap;
|
|
232
245
|
const flowsArray = Array.from(flows.values());
|
|
233
246
|
const flowsWithSteps = await Promise.all(
|
|
234
247
|
flowsArray.map(async (flow) => {
|
|
235
248
|
const configurableSteps = await Promise.all(
|
|
236
249
|
Object.entries(flow.steps).map(async ([stepKey, stepDefinition]) => {
|
|
237
|
-
var
|
|
250
|
+
var _a2;
|
|
238
251
|
const actionStep = stepDefinition;
|
|
239
252
|
if (actionStep.hideInSettings) {
|
|
240
253
|
return null;
|
|
@@ -244,7 +257,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
|
|
|
244
257
|
let stepTitle = actionStep.title;
|
|
245
258
|
if (actionStep.use) {
|
|
246
259
|
try {
|
|
247
|
-
const action = (
|
|
260
|
+
const action = (_a2 = targetModel.getAction) == null ? void 0 : _a2.call(targetModel, actionStep.use);
|
|
248
261
|
hasActionUiSchema = action && action.uiSchema != null;
|
|
249
262
|
stepTitle = stepTitle || (action == null ? void 0 : action.title);
|
|
250
263
|
} catch (error) {
|
package/lib/flowEngine.d.ts
CHANGED
|
@@ -234,6 +234,11 @@ export declare class FlowEngine {
|
|
|
234
234
|
delegateToParent?: boolean;
|
|
235
235
|
delegate?: FlowContext;
|
|
236
236
|
}): T;
|
|
237
|
+
/**
|
|
238
|
+
* 按类上的 resolveUse 链路解析最终用于实例化的模型类。
|
|
239
|
+
* 允许模型类根据上下文动态指定实际使用的类,支持多级 resolveUse。
|
|
240
|
+
*/
|
|
241
|
+
private _resolveModelClass;
|
|
237
242
|
/**
|
|
238
243
|
* 尝试应用当前模型可用 flow 的 defaultParams(如果存在)到 model.stepParams。
|
|
239
244
|
* 仅对尚未存在的步骤参数进行填充,不覆盖已有值。
|
package/lib/flowEngine.js
CHANGED
|
@@ -392,7 +392,10 @@ const _FlowEngine = class _FlowEngine {
|
|
|
392
392
|
*/
|
|
393
393
|
createModel(options, extra) {
|
|
394
394
|
const { parentId, uid, use: modelClassName, subModels } = options;
|
|
395
|
-
const ModelClass =
|
|
395
|
+
const ModelClass = this._resolveModelClass(
|
|
396
|
+
typeof modelClassName === "string" ? this.getModelClass(modelClassName) : modelClassName,
|
|
397
|
+
options
|
|
398
|
+
);
|
|
396
399
|
if (uid && this._modelInstances.has(uid)) {
|
|
397
400
|
return this._modelInstances.get(uid);
|
|
398
401
|
}
|
|
@@ -424,6 +427,41 @@ const _FlowEngine = class _FlowEngine {
|
|
|
424
427
|
modelInstance._createSubModels(options.subModels);
|
|
425
428
|
return modelInstance;
|
|
426
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* 按类上的 resolveUse 链路解析最终用于实例化的模型类。
|
|
432
|
+
* 允许模型类根据上下文动态指定实际使用的类,支持多级 resolveUse。
|
|
433
|
+
*/
|
|
434
|
+
_resolveModelClass(initial, options) {
|
|
435
|
+
let current = initial;
|
|
436
|
+
const visited = /* @__PURE__ */ new Set();
|
|
437
|
+
while (current) {
|
|
438
|
+
if (visited.has(current)) {
|
|
439
|
+
console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
visited.add(current);
|
|
443
|
+
const resolver = current == null ? void 0 : current.resolveUse;
|
|
444
|
+
if (typeof resolver !== "function") {
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
const resolved = resolver(options, this);
|
|
448
|
+
if (!resolved || resolved === current) {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
let next;
|
|
452
|
+
if (typeof resolved === "string") {
|
|
453
|
+
next = this.getModelClass(resolved);
|
|
454
|
+
if (!next) {
|
|
455
|
+
console.warn(`FlowEngine: resolveUse returned '${resolved}' but no model is registered under that name.`);
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
next = resolved;
|
|
460
|
+
}
|
|
461
|
+
current = next;
|
|
462
|
+
}
|
|
463
|
+
return current;
|
|
464
|
+
}
|
|
427
465
|
/**
|
|
428
466
|
* 尝试应用当前模型可用 flow 的 defaultParams(如果存在)到 model.stepParams。
|
|
429
467
|
* 仅对尚未存在的步骤参数进行填充,不覆盖已有值。
|
|
@@ -500,6 +538,7 @@ const _FlowEngine = class _FlowEngine {
|
|
|
500
538
|
return false;
|
|
501
539
|
}
|
|
502
540
|
const modelInstance = this._modelInstances.get(uid);
|
|
541
|
+
modelInstance.invalidateFlowCache(void 0, true);
|
|
503
542
|
modelInstance.clearForks();
|
|
504
543
|
if ((_a = modelInstance.parent) == null ? void 0 : _a.subModels) {
|
|
505
544
|
for (const subKey in modelInstance.parent.subModels) {
|
package/lib/flowSettings.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Emitter } from '../emitter';
|
|
|
11
11
|
import { InstanceFlowRegistry } from '../flow-registry/InstanceFlowRegistry';
|
|
12
12
|
import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
|
|
13
13
|
import { FlowEngine } from '../flowEngine';
|
|
14
|
-
import type { ActionDefinition, ArrayElementType, CreateModelOptions, CreateSubModelOptions, DefaultStructure, FlowDefinitionOptions, FlowModelMeta, FlowModelOptions, ModelConstructor, ParamObject, ParentFlowModel, PersistOptions, StepParams } from '../types';
|
|
14
|
+
import type { ActionDefinition, ArrayElementType, CreateModelOptions, CreateSubModelOptions, DefaultStructure, FlowDefinitionOptions, FlowModelMeta, FlowModelOptions, ModelConstructor, ParamObject, ParentFlowModel, PersistOptions, RegisteredModelClassName, StepParams } from '../types';
|
|
15
15
|
import { IModelComponentProps, ReadonlyModelProps } from '../types';
|
|
16
16
|
import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
|
|
17
17
|
import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
|
|
@@ -97,6 +97,11 @@ export declare class FlowModel<Structure extends DefaultStructure = DefaultStruc
|
|
|
97
97
|
static get globalFlowRegistry(): GlobalFlowRegistry;
|
|
98
98
|
protected static get actionRegistry(): ModelActionRegistry;
|
|
99
99
|
protected static get eventRegistry(): ModelEventRegistry;
|
|
100
|
+
/**
|
|
101
|
+
* 动态解析实际要实例化的模型类;可在子类中覆盖。
|
|
102
|
+
* 返回注册名或构造器,支持在 FlowEngine 中继续沿链解析。
|
|
103
|
+
*/
|
|
104
|
+
static resolveUse?(options: CreateModelOptions, engine: FlowEngine): RegisteredModelClassName | ModelConstructor | void;
|
|
100
105
|
/**
|
|
101
106
|
* 注册仅当前 FlowModel 类及其子类可用的 Action。
|
|
102
107
|
* 该注册是类级别的,不会影响全局(FlowEngine)的 Action 注册。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.46",
|
|
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.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.0.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.0.0-alpha.46",
|
|
12
|
+
"@nocobase/shared": "2.0.0-alpha.46",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dayjs": "^1.11.9",
|
|
15
15
|
"dompurify": "^3.0.2",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
],
|
|
37
37
|
"author": "NocoBase Team",
|
|
38
38
|
"license": "AGPL-3.0",
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "ebf3cbeea4957a914e3157f3c6b570dba685c46f"
|
|
40
40
|
}
|
|
@@ -222,8 +222,10 @@ const FlowModelRendererCore: React.FC<{
|
|
|
222
222
|
};
|
|
223
223
|
|
|
224
224
|
// 如果不显示流程设置,直接返回模型内容(可能包装 ErrorBoundary)
|
|
225
|
-
//
|
|
226
|
-
const
|
|
225
|
+
// 当模型类或 use 变化时重挂载内容,规避组件内部状态残留
|
|
226
|
+
const rawUse = (model as any)?.use;
|
|
227
|
+
const resolvedName = (model as any)?.constructor?.name || model.uid;
|
|
228
|
+
const contentKey = typeof rawUse === 'string' ? `${rawUse}:${model.uid}` : `${resolvedName}:${model.uid}`;
|
|
227
229
|
|
|
228
230
|
if (!showFlowSettings) {
|
|
229
231
|
return wrapWithErrorBoundary(
|
|
@@ -267,8 +267,23 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
|
|
|
267
267
|
const getModelConfigurableFlowsAndSteps = useCallback(
|
|
268
268
|
async (targetModel: FlowModel, modelKey?: string): Promise<FlowInfo[]> => {
|
|
269
269
|
try {
|
|
270
|
-
// 仅使用静态流(类级全局注册的 flows
|
|
271
|
-
const
|
|
270
|
+
// 仅使用静态流(类级全局注册的 flows)
|
|
271
|
+
const flowsMap = new Map((targetModel.constructor as typeof FlowModel).globalFlowRegistry.getFlows());
|
|
272
|
+
|
|
273
|
+
// 如果有原始 use 且与当前类不同,合并原始模型类的静态 flows(用于入口模型 resolveUse 场景)
|
|
274
|
+
const originUse = targetModel?.use;
|
|
275
|
+
if (typeof originUse === 'string' && originUse !== targetModel.constructor.name) {
|
|
276
|
+
const originCls = targetModel.flowEngine?.getModelClass(originUse) as typeof FlowModel | undefined;
|
|
277
|
+
if (originCls?.globalFlowRegistry) {
|
|
278
|
+
for (const [k, v] of originCls.globalFlowRegistry.getFlows()) {
|
|
279
|
+
if (!flowsMap.has(k)) {
|
|
280
|
+
flowsMap.set(k, v);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const flows = flowsMap;
|
|
272
287
|
|
|
273
288
|
const flowsArray = Array.from(flows.values());
|
|
274
289
|
|
|
@@ -422,4 +422,59 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
|
|
|
422
422
|
menu.onClick?.({ key: 'copy-pop-uid:items[0]:popupSettings:stage' });
|
|
423
423
|
expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith('child-2');
|
|
424
424
|
});
|
|
425
|
+
|
|
426
|
+
it('merges static flows from origin use when instance class differs', async () => {
|
|
427
|
+
class TargetModel extends FlowModel {
|
|
428
|
+
static setupFlows() {
|
|
429
|
+
this.registerFlow({
|
|
430
|
+
key: 'targetFlow',
|
|
431
|
+
title: 'Target Flow',
|
|
432
|
+
steps: {
|
|
433
|
+
targetStep: { title: 'Target Step', uiSchema: { x: { type: 'string', 'x-component': 'Input' } } },
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
class EntryModel extends FlowModel {
|
|
439
|
+
static resolveUse() {
|
|
440
|
+
return TargetModel;
|
|
441
|
+
}
|
|
442
|
+
static setupFlows() {
|
|
443
|
+
this.registerFlow({
|
|
444
|
+
key: 'originFlow',
|
|
445
|
+
title: 'Origin Flow',
|
|
446
|
+
steps: {
|
|
447
|
+
originStep: { title: 'Origin Step', uiSchema: { y: { type: 'string', 'x-component': 'Input' } } },
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
TargetModel.setupFlows();
|
|
454
|
+
EntryModel.setupFlows();
|
|
455
|
+
|
|
456
|
+
const engine = new FlowEngine();
|
|
457
|
+
engine.registerModels({ EntryModel, TargetModel });
|
|
458
|
+
|
|
459
|
+
const model = engine.createModel({ use: 'EntryModel', uid: 'merge-origin', flowEngine: engine });
|
|
460
|
+
|
|
461
|
+
render(
|
|
462
|
+
React.createElement(
|
|
463
|
+
ConfigProvider as any,
|
|
464
|
+
null,
|
|
465
|
+
React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
|
|
466
|
+
),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
await waitFor(() => {
|
|
470
|
+
const menu = (globalThis as any).__lastDropdownMenu;
|
|
471
|
+
expect(menu).toBeTruthy();
|
|
472
|
+
const items = (menu?.items || []) as any[];
|
|
473
|
+
const groups = items.filter((it) => it.type === 'group').map((it) => String(it.label));
|
|
474
|
+
expect(groups).toContain('Origin Flow');
|
|
475
|
+
expect(groups).toContain('Target Flow');
|
|
476
|
+
expect(items.some((it) => String(it.key || '').startsWith('originFlow:originStep'))).toBe(true);
|
|
477
|
+
expect(items.some((it) => String(it.key || '').startsWith('targetFlow:targetStep'))).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
425
480
|
});
|
package/src/flowEngine.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
ModelConstructor,
|
|
31
31
|
PersistOptions,
|
|
32
32
|
ResourceType,
|
|
33
|
+
RegisteredModelClassName,
|
|
33
34
|
} from './types';
|
|
34
35
|
import { isInheritedFrom } from './utils';
|
|
35
36
|
|
|
@@ -458,7 +459,10 @@ export class FlowEngine {
|
|
|
458
459
|
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
459
460
|
): T {
|
|
460
461
|
const { parentId, uid, use: modelClassName, subModels } = options;
|
|
461
|
-
const ModelClass =
|
|
462
|
+
const ModelClass = this._resolveModelClass(
|
|
463
|
+
typeof modelClassName === 'string' ? this.getModelClass(modelClassName) : modelClassName,
|
|
464
|
+
options,
|
|
465
|
+
);
|
|
462
466
|
|
|
463
467
|
if (uid && this._modelInstances.has(uid)) {
|
|
464
468
|
return this._modelInstances.get(uid) as T;
|
|
@@ -505,6 +509,53 @@ export class FlowEngine {
|
|
|
505
509
|
return modelInstance as T;
|
|
506
510
|
}
|
|
507
511
|
|
|
512
|
+
/**
|
|
513
|
+
* 按类上的 resolveUse 链路解析最终用于实例化的模型类。
|
|
514
|
+
* 允许模型类根据上下文动态指定实际使用的类,支持多级 resolveUse。
|
|
515
|
+
*/
|
|
516
|
+
private _resolveModelClass(
|
|
517
|
+
initial: ModelConstructor | undefined,
|
|
518
|
+
options: CreateModelOptions,
|
|
519
|
+
): ModelConstructor | undefined {
|
|
520
|
+
let current = initial;
|
|
521
|
+
const visited = new Set<ModelConstructor>();
|
|
522
|
+
|
|
523
|
+
while (current) {
|
|
524
|
+
if (visited.has(current)) {
|
|
525
|
+
console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
visited.add(current);
|
|
529
|
+
|
|
530
|
+
const resolver = (current as any)?.resolveUse as
|
|
531
|
+
| ((opts: CreateModelOptions, engine: FlowEngine) => RegisteredModelClassName | ModelConstructor | void)
|
|
532
|
+
| undefined;
|
|
533
|
+
if (typeof resolver !== 'function') {
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const resolved = resolver(options, this);
|
|
538
|
+
if (!resolved || resolved === current) {
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let next: ModelConstructor | undefined;
|
|
543
|
+
if (typeof resolved === 'string') {
|
|
544
|
+
next = this.getModelClass(resolved);
|
|
545
|
+
if (!next) {
|
|
546
|
+
console.warn(`FlowEngine: resolveUse returned '${resolved}' but no model is registered under that name.`);
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
next = resolved;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
current = next;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return current;
|
|
557
|
+
}
|
|
558
|
+
|
|
508
559
|
/**
|
|
509
560
|
* 尝试应用当前模型可用 flow 的 defaultParams(如果存在)到 model.stepParams。
|
|
510
561
|
* 仅对尚未存在的步骤参数进行填充,不覆盖已有值。
|
|
@@ -591,6 +642,8 @@ export class FlowEngine {
|
|
|
591
642
|
return false;
|
|
592
643
|
}
|
|
593
644
|
const modelInstance = this._modelInstances.get(uid) as FlowModel;
|
|
645
|
+
// Ensure any cached beforeRender results tied to this uid are cleared before removal.
|
|
646
|
+
modelInstance.invalidateFlowCache(undefined, true);
|
|
594
647
|
modelInstance.clearForks();
|
|
595
648
|
// 从父模型中移除当前模型的引用
|
|
596
649
|
if (modelInstance.parent?.subModels) {
|
package/src/flowSettings.ts
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../../flowEngine';
|
|
12
|
+
import { ErrorFlowModel, FlowModel } from '../flowModel';
|
|
13
|
+
|
|
14
|
+
describe('FlowEngine.createModel resolveUse hook', () => {
|
|
15
|
+
let engine: FlowEngine;
|
|
16
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
engine = new FlowEngine();
|
|
20
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
warnSpy.mockRestore();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should use resolveUse returning registered name', () => {
|
|
28
|
+
class DynamicTarget extends FlowModel {}
|
|
29
|
+
class DynamicEntry extends FlowModel {
|
|
30
|
+
static resolveUse() {
|
|
31
|
+
return 'DynamicTarget';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
engine.registerModels({ DynamicEntry, DynamicTarget });
|
|
36
|
+
|
|
37
|
+
const model = engine.createModel({ use: 'DynamicEntry', uid: 'dynamic-entry', flowEngine: engine });
|
|
38
|
+
|
|
39
|
+
expect(model).toBeInstanceOf(DynamicTarget);
|
|
40
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should chain resolveUse until final constructor', () => {
|
|
44
|
+
class ChainLeaf extends FlowModel {}
|
|
45
|
+
class ChainMid extends FlowModel {
|
|
46
|
+
static resolveUse() {
|
|
47
|
+
return ChainLeaf;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class ChainStart extends FlowModel {
|
|
51
|
+
static resolveUse() {
|
|
52
|
+
return 'ChainMid';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
engine.registerModels({ ChainLeaf, ChainMid, ChainStart });
|
|
57
|
+
|
|
58
|
+
const model = engine.createModel({ use: 'ChainStart', uid: 'chain-start', flowEngine: engine });
|
|
59
|
+
|
|
60
|
+
expect(model).toBeInstanceOf(ChainLeaf);
|
|
61
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
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
|
+
test('should fall back to ErrorFlowModel when resolveUse returns unregistered name', () => {
|
|
80
|
+
class MissingTargetEntry extends FlowModel {
|
|
81
|
+
static resolveUse() {
|
|
82
|
+
return 'NoSuchModel';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
engine.registerModels({ MissingTargetEntry });
|
|
87
|
+
|
|
88
|
+
const model = engine.createModel({ use: 'MissingTargetEntry', uid: 'missing-target', flowEngine: engine });
|
|
89
|
+
|
|
90
|
+
expect(model).toBeInstanceOf(ErrorFlowModel);
|
|
91
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
92
|
+
expect.stringContaining("resolveUse returned 'NoSuchModel' but no model is registered under that name."),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('should keep original use in serialized data after resolveUse', () => {
|
|
97
|
+
class DynamicTarget extends FlowModel {}
|
|
98
|
+
class DynamicEntry extends FlowModel {
|
|
99
|
+
static resolveUse() {
|
|
100
|
+
return DynamicTarget;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
engine.registerModels({ DynamicEntry, DynamicTarget });
|
|
105
|
+
|
|
106
|
+
const model = engine.createModel({ use: 'DynamicEntry', uid: 'dynamic-entry', flowEngine: engine });
|
|
107
|
+
const serialized = model.serialize();
|
|
108
|
+
|
|
109
|
+
expect(model).toBeInstanceOf(DynamicTarget);
|
|
110
|
+
expect(serialized.use).toBe('DynamicEntry');
|
|
111
|
+
});
|
|
112
|
+
});
|
package/src/models/flowModel.tsx
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
ParamObject,
|
|
32
32
|
ParentFlowModel,
|
|
33
33
|
PersistOptions,
|
|
34
|
+
RegisteredModelClassName,
|
|
34
35
|
StepDefinition,
|
|
35
36
|
StepParams,
|
|
36
37
|
} from '../types';
|
|
@@ -318,6 +319,15 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
318
319
|
return registry;
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
/**
|
|
323
|
+
* 动态解析实际要实例化的模型类;可在子类中覆盖。
|
|
324
|
+
* 返回注册名或构造器,支持在 FlowEngine 中继续沿链解析。
|
|
325
|
+
*/
|
|
326
|
+
static resolveUse?(
|
|
327
|
+
options: CreateModelOptions,
|
|
328
|
+
engine: FlowEngine,
|
|
329
|
+
): RegisteredModelClassName | ModelConstructor | void;
|
|
330
|
+
|
|
321
331
|
/**
|
|
322
332
|
* 注册仅当前 FlowModel 类及其子类可用的 Action。
|
|
323
333
|
* 该注册是类级别的,不会影响全局(FlowEngine)的 Action 注册。
|