@nocobase/flow-engine 2.1.0-alpha.40 → 2.1.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/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +84 -32
- package/lib/components/subModel/LazyDropdown.js +208 -16
- package/lib/components/subModel/utils.d.ts +1 -0
- package/lib/components/subModel/utils.js +6 -2
- package/lib/data-source/index.d.ts +9 -0
- package/lib/data-source/index.js +12 -0
- package/lib/executor/FlowExecutor.js +0 -3
- package/lib/flowContext.d.ts +6 -1
- package/lib/flowContext.js +38 -6
- package/lib/flowEngine.d.ts +4 -3
- package/lib/flowEngine.js +72 -40
- package/lib/models/flowModel.js +48 -16
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
- package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/base.js +464 -29
- package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
- package/lib/runjs-context/contexts/elementDoc.js +152 -0
- package/lib/utils/loadedPageCache.d.ts +24 -0
- package/lib/utils/loadedPageCache.js +139 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +28 -4
- package/lib/views/ViewNavigation.d.ts +12 -2
- package/lib/views/ViewNavigation.js +22 -7
- package/lib/views/createViewMeta.js +114 -50
- package/lib/views/inheritLayoutContext.d.ts +10 -0
- package/lib/views/inheritLayoutContext.js +50 -0
- package/lib/views/useDialog.js +2 -0
- package/lib/views/useDrawer.js +2 -0
- package/lib/views/usePage.js +2 -0
- package/package.json +4 -4
- package/src/FlowContextProvider.tsx +9 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -1
- package/src/__tests__/flowContext.test.ts +23 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
- package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
- package/src/__tests__/runjsContext.test.ts +18 -0
- package/src/__tests__/runjsContextImplementations.test.ts +9 -2
- package/src/__tests__/runjsLocales.test.ts +6 -5
- package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +90 -38
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +155 -5
- package/src/components/subModel/LazyDropdown.tsx +237 -16
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +254 -1
- package/src/components/subModel/utils.ts +6 -1
- package/src/data-source/index.ts +18 -0
- package/src/executor/FlowExecutor.ts +0 -3
- package/src/executor/__tests__/flowExecutor.test.ts +26 -0
- package/src/flowContext.ts +43 -6
- package/src/flowEngine.ts +75 -38
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +46 -62
- package/src/models/flowModel.tsx +65 -32
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
- package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/base.ts +467 -31
- package/src/runjs-context/contexts/elementDoc.ts +130 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
- package/src/utils/loadedPageCache.ts +147 -0
- package/src/utils/parsePathnameToViewParams.ts +45 -5
- package/src/views/ViewNavigation.ts +40 -7
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/useDialog.tsx +2 -0
- package/src/views/useDrawer.tsx +2 -0
- package/src/views/usePage.tsx +2 -0
package/src/flowEngine.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { APIResource, FlowResource, MultiRecordResource, SingleRecordResource, S
|
|
|
20
20
|
import { Emitter } from './emitter';
|
|
21
21
|
import ModelOperationScheduler from './scheduler/ModelOperationScheduler';
|
|
22
22
|
import type { ScheduleOptions, ScheduledCancel } from './scheduler/ModelOperationScheduler';
|
|
23
|
+
import { createLoadedPageCache } from './utils/loadedPageCache';
|
|
23
24
|
import type {
|
|
24
25
|
ActionDefinition,
|
|
25
26
|
ApplyFlowCacheEntry,
|
|
@@ -39,6 +40,8 @@ import type {
|
|
|
39
40
|
} from './types';
|
|
40
41
|
import { isInheritedFrom } from './utils';
|
|
41
42
|
|
|
43
|
+
const getFlowEngineLoggerLevel = () => (process.env.NODE_ENV === 'production' ? 'warn' : 'trace');
|
|
44
|
+
|
|
42
45
|
/**
|
|
43
46
|
* FlowEngine is the core class of the flow engine, responsible for managing flow models, actions, model repository, and more.
|
|
44
47
|
* It provides capabilities for registering, creating, finding, persisting, replacing, and moving models.
|
|
@@ -133,6 +136,8 @@ export class FlowEngine {
|
|
|
133
136
|
*/
|
|
134
137
|
private _savingModels = new Map<string, Promise<any>>();
|
|
135
138
|
|
|
139
|
+
private _loadedPageCache = createLoadedPageCache();
|
|
140
|
+
|
|
136
141
|
/**
|
|
137
142
|
* Flow engine context object.
|
|
138
143
|
* @private
|
|
@@ -213,7 +218,7 @@ export class FlowEngine {
|
|
|
213
218
|
MultiRecordResource,
|
|
214
219
|
});
|
|
215
220
|
this.logger = pino({
|
|
216
|
-
level:
|
|
221
|
+
level: getFlowEngineLoggerLevel(),
|
|
217
222
|
browser: {
|
|
218
223
|
write: {
|
|
219
224
|
fatal: (o) => console.trace(o),
|
|
@@ -1009,7 +1014,6 @@ export class FlowEngine {
|
|
|
1009
1014
|
|
|
1010
1015
|
while (current) {
|
|
1011
1016
|
if (visited.has(current)) {
|
|
1012
|
-
console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
|
|
1013
1017
|
break;
|
|
1014
1018
|
}
|
|
1015
1019
|
visited.add(current);
|
|
@@ -1128,7 +1132,7 @@ export class FlowEngine {
|
|
|
1128
1132
|
*/
|
|
1129
1133
|
public removeModel(uid: string): boolean {
|
|
1130
1134
|
if (!this._modelInstances.has(uid)) {
|
|
1131
|
-
|
|
1135
|
+
this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
|
|
1132
1136
|
return false;
|
|
1133
1137
|
}
|
|
1134
1138
|
const modelInstance = this._modelInstances.get(uid) as FlowModel;
|
|
@@ -1338,7 +1342,8 @@ export class FlowEngine {
|
|
|
1338
1342
|
async loadModel<T extends FlowModel = FlowModel>(options): Promise<T | null> {
|
|
1339
1343
|
if (!this.ensureModelRepository()) return;
|
|
1340
1344
|
const refresh = !!options?.refresh;
|
|
1341
|
-
|
|
1345
|
+
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1346
|
+
if (!refresh && !bypassLoadedPageCache) {
|
|
1342
1347
|
const model = this.findModelByParentId(options.parentId, options.subKey);
|
|
1343
1348
|
if (model) {
|
|
1344
1349
|
return model as T;
|
|
@@ -1349,15 +1354,24 @@ export class FlowEngine {
|
|
|
1349
1354
|
}
|
|
1350
1355
|
}
|
|
1351
1356
|
const data = await this._modelRepository.findOne(options);
|
|
1352
|
-
if (!data?.uid)
|
|
1353
|
-
|
|
1354
|
-
|
|
1357
|
+
if (!data?.uid) {
|
|
1358
|
+
if (bypassLoadedPageCache) {
|
|
1359
|
+
this._loadedPageCache.clear(options);
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
if (refresh || bypassLoadedPageCache) {
|
|
1355
1364
|
const existing = this.getModel(data.uid);
|
|
1356
1365
|
if (existing) {
|
|
1357
1366
|
this.removeModelWithSubModels(existing.uid);
|
|
1358
1367
|
}
|
|
1359
1368
|
}
|
|
1360
|
-
|
|
1369
|
+
const model = await this.createModelAsync<T>(data as any);
|
|
1370
|
+
if (bypassLoadedPageCache) {
|
|
1371
|
+
this._loadedPageCache.mountModelToParent(model, true);
|
|
1372
|
+
this._loadedPageCache.clear(options);
|
|
1373
|
+
}
|
|
1374
|
+
return model;
|
|
1361
1375
|
}
|
|
1362
1376
|
|
|
1363
1377
|
/**
|
|
@@ -1397,22 +1411,31 @@ export class FlowEngine {
|
|
|
1397
1411
|
): Promise<T | null> {
|
|
1398
1412
|
if (!this.ensureModelRepository()) return;
|
|
1399
1413
|
const { uid, parentId, subKey } = options;
|
|
1400
|
-
|
|
1414
|
+
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1415
|
+
if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
|
|
1401
1416
|
return this._modelInstances.get(uid) as T;
|
|
1402
1417
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1418
|
+
if (!bypassLoadedPageCache) {
|
|
1419
|
+
const m = this.findModelByParentId<T>(parentId, subKey);
|
|
1420
|
+
if (m) {
|
|
1421
|
+
return m;
|
|
1422
|
+
}
|
|
1407
1423
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1424
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1425
|
+
if (hydrated) {
|
|
1426
|
+
return hydrated;
|
|
1427
|
+
}
|
|
1411
1428
|
}
|
|
1412
1429
|
|
|
1413
1430
|
const data = await this._modelRepository.findOne(options);
|
|
1414
1431
|
let model: T | null = null;
|
|
1415
1432
|
if (data?.uid) {
|
|
1433
|
+
if (bypassLoadedPageCache) {
|
|
1434
|
+
const existing = this.getModel(data.uid);
|
|
1435
|
+
if (existing) {
|
|
1436
|
+
this.removeModelWithSubModels(existing.uid);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1416
1439
|
model = await this.createModelAsync<T>(data as any, extra);
|
|
1417
1440
|
} else {
|
|
1418
1441
|
model = await this.createModelAsync<T>(options, extra);
|
|
@@ -1420,18 +1443,9 @@ export class FlowEngine {
|
|
|
1420
1443
|
await model.save();
|
|
1421
1444
|
}
|
|
1422
1445
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
});
|
|
1427
|
-
if (subModel) {
|
|
1428
|
-
return model;
|
|
1429
|
-
}
|
|
1430
|
-
if (model.subType === 'array') {
|
|
1431
|
-
model.parent.addSubModel(model.subKey, model);
|
|
1432
|
-
} else {
|
|
1433
|
-
model.parent.setSubModel(model.subKey, model);
|
|
1434
|
-
}
|
|
1446
|
+
this._loadedPageCache.mountModelToParent(model, bypassLoadedPageCache);
|
|
1447
|
+
if (bypassLoadedPageCache) {
|
|
1448
|
+
this._loadedPageCache.clear(options);
|
|
1435
1449
|
}
|
|
1436
1450
|
return model;
|
|
1437
1451
|
}
|
|
@@ -1451,6 +1465,9 @@ export class FlowEngine {
|
|
|
1451
1465
|
if (!this.ensureModelRepository()) return;
|
|
1452
1466
|
|
|
1453
1467
|
const modelUid = model.uid;
|
|
1468
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(model, {
|
|
1469
|
+
force: !!options?.onlyStepParams,
|
|
1470
|
+
});
|
|
1454
1471
|
|
|
1455
1472
|
// 如果这个 model 正在保存中,返回现有的保存 Promise
|
|
1456
1473
|
if (this._savingModels.has(modelUid)) {
|
|
@@ -1464,6 +1481,7 @@ export class FlowEngine {
|
|
|
1464
1481
|
|
|
1465
1482
|
try {
|
|
1466
1483
|
const result = await savePromise;
|
|
1484
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1467
1485
|
return result;
|
|
1468
1486
|
} finally {
|
|
1469
1487
|
// 无论成功还是失败,都要清除保存状态
|
|
@@ -1500,11 +1518,16 @@ export class FlowEngine {
|
|
|
1500
1518
|
* @returns {Promise<boolean>} Whether destroyed successfully
|
|
1501
1519
|
*/
|
|
1502
1520
|
async destroyModel(uid: string) {
|
|
1503
|
-
|
|
1521
|
+
const modelInstance = this._modelInstances.get(uid) as FlowModel;
|
|
1522
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(modelInstance);
|
|
1523
|
+
const hasModelRepository = this.ensureModelRepository();
|
|
1524
|
+
if (hasModelRepository) {
|
|
1504
1525
|
await this._modelRepository.destroy(uid);
|
|
1505
1526
|
}
|
|
1506
1527
|
|
|
1507
|
-
|
|
1528
|
+
if (hasModelRepository) {
|
|
1529
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1530
|
+
}
|
|
1508
1531
|
const parent = modelInstance?.parent;
|
|
1509
1532
|
const result = this.removeModel(uid);
|
|
1510
1533
|
parent && parent.emitter.emit('onSubModelDestroyed', modelInstance);
|
|
@@ -1608,17 +1631,25 @@ export class FlowEngine {
|
|
|
1608
1631
|
|
|
1609
1632
|
/**
|
|
1610
1633
|
* Move a model instance within its parent model.
|
|
1611
|
-
* @param {
|
|
1612
|
-
* @param {
|
|
1634
|
+
* @param {string | number} sourceId Source model UID
|
|
1635
|
+
* @param {string | number} targetId Target model UID
|
|
1613
1636
|
* @returns {Promise<void>} No return value
|
|
1614
1637
|
*/
|
|
1615
|
-
async moveModel(sourceId:
|
|
1616
|
-
const
|
|
1617
|
-
const
|
|
1638
|
+
async moveModel(sourceId: string | number, targetId: string | number, options?: PersistOptions): Promise<void> {
|
|
1639
|
+
const sourceUid = String(sourceId);
|
|
1640
|
+
const targetUid = String(targetId);
|
|
1641
|
+
if (!sourceUid || !targetUid || sourceUid === targetUid) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const sourceModel = this.getModel(sourceUid);
|
|
1646
|
+
const targetModel = this.getModel(targetUid);
|
|
1618
1647
|
if (!sourceModel || !targetModel) {
|
|
1619
1648
|
console.warn(`FlowEngine: Cannot move model. Source or target model not found.`);
|
|
1620
1649
|
return;
|
|
1621
1650
|
}
|
|
1651
|
+
let position: 'before' | 'after' = 'after';
|
|
1652
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(sourceModel);
|
|
1622
1653
|
const move = (sourceModel: FlowModel, targetModel: FlowModel) => {
|
|
1623
1654
|
if (!sourceModel.parent || !targetModel.parent || sourceModel.parent !== targetModel.parent) {
|
|
1624
1655
|
console.error('FlowModel.moveTo: Both models must have the same parent to perform move operation.');
|
|
@@ -1648,6 +1679,8 @@ export class FlowEngine {
|
|
|
1648
1679
|
return false;
|
|
1649
1680
|
}
|
|
1650
1681
|
|
|
1682
|
+
position = currentIndex < targetIndex ? 'after' : 'before';
|
|
1683
|
+
|
|
1651
1684
|
// 使用splice直接移动数组元素(O(n)比排序O(n log n)更快)
|
|
1652
1685
|
const [movedModel] = subModelsCopy.splice(currentIndex, 1);
|
|
1653
1686
|
subModelsCopy.splice(targetIndex, 0, movedModel);
|
|
@@ -1662,10 +1695,14 @@ export class FlowEngine {
|
|
|
1662
1695
|
|
|
1663
1696
|
return true;
|
|
1664
1697
|
};
|
|
1665
|
-
move(sourceModel, targetModel);
|
|
1698
|
+
const moved = move(sourceModel, targetModel);
|
|
1699
|
+
if (!moved) {
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1666
1703
|
if (options?.persist !== false && this.ensureModelRepository()) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1704
|
+
await this._modelRepository.move(sourceUid, targetUid, position);
|
|
1705
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1669
1706
|
}
|
|
1670
1707
|
// 触发事件以通知其他部分模型已移动
|
|
1671
1708
|
sourceModel.parent.emitter.emit('onSubModelMoved', { source: sourceModel, target: targetModel });
|
|
@@ -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() {
|
|
@@ -313,6 +313,19 @@ describe('FlowModel', () => {
|
|
|
313
313
|
|
|
314
314
|
model.emitter.off('onStepParamsChanged', listener);
|
|
315
315
|
});
|
|
316
|
+
|
|
317
|
+
test('should not emit onStepParamsChanged when params are unchanged', () => {
|
|
318
|
+
const listener = vi.fn();
|
|
319
|
+
model.emitter.on('onStepParamsChanged', listener);
|
|
320
|
+
|
|
321
|
+
model.setStepParams('testFlow', 'step1', { param1: 'value1' });
|
|
322
|
+
model.setStepParams('testFlow', { step1: { param1: 'value1' } });
|
|
323
|
+
model.setStepParams({ testFlow: { step1: { param1: 'value1' } } });
|
|
324
|
+
|
|
325
|
+
expect(listener).not.toHaveBeenCalled();
|
|
326
|
+
|
|
327
|
+
model.emitter.off('onStepParamsChanged', listener);
|
|
328
|
+
});
|
|
316
329
|
});
|
|
317
330
|
});
|
|
318
331
|
|
|
@@ -370,15 +383,17 @@ describe('FlowModel', () => {
|
|
|
370
383
|
};
|
|
371
384
|
|
|
372
385
|
TestFlowModel.registerFlow(exitFlow);
|
|
373
|
-
const
|
|
386
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
374
387
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
expect(result).toBeInstanceOf(FlowExitAllException);
|
|
378
|
-
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
379
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
|
|
388
|
+
try {
|
|
389
|
+
const result = await model.applyFlow('exitFlow');
|
|
380
390
|
|
|
381
|
-
|
|
391
|
+
expect(result).toBeInstanceOf(FlowExitAllException);
|
|
392
|
+
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
393
|
+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
|
|
394
|
+
} finally {
|
|
395
|
+
loggerSpy.mockRestore();
|
|
396
|
+
}
|
|
382
397
|
});
|
|
383
398
|
|
|
384
399
|
test('should handle ctx.exit() as FlowExitAllException in beforeRender dispatch', async () => {
|
|
@@ -474,15 +489,17 @@ describe('FlowModel', () => {
|
|
|
474
489
|
};
|
|
475
490
|
|
|
476
491
|
TestFlowModel.registerFlow(exitFlow);
|
|
477
|
-
const
|
|
492
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
478
493
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
expect(result).toBeInstanceOf(FlowExitAllException);
|
|
482
|
-
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
483
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
|
|
494
|
+
try {
|
|
495
|
+
const result = await model.applyFlow('exitFlow');
|
|
484
496
|
|
|
485
|
-
|
|
497
|
+
expect(result).toBeInstanceOf(FlowExitAllException);
|
|
498
|
+
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
499
|
+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
|
|
500
|
+
} finally {
|
|
501
|
+
loggerSpy.mockRestore();
|
|
502
|
+
}
|
|
486
503
|
});
|
|
487
504
|
|
|
488
505
|
test('should propagate step execution errors', async () => {
|
|
@@ -546,34 +563,6 @@ describe('FlowModel', () => {
|
|
|
546
563
|
|
|
547
564
|
loggerSpy.mockRestore();
|
|
548
565
|
});
|
|
549
|
-
|
|
550
|
-
test('should warn and skip step when use and handler are both missing', async () => {
|
|
551
|
-
const warnSpy = vi.spyOn(model.context.logger, 'warn').mockImplementation(() => {});
|
|
552
|
-
const errorSpy = vi.spyOn(model.context.logger, 'error').mockImplementation(() => {});
|
|
553
|
-
|
|
554
|
-
TestFlowModel.registerFlow({
|
|
555
|
-
key: 'settingsOnlyFlow',
|
|
556
|
-
steps: {
|
|
557
|
-
edit: {
|
|
558
|
-
title: 'Edit',
|
|
559
|
-
uiSchema: {},
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
const result = await model.applyFlow('settingsOnlyFlow');
|
|
565
|
-
|
|
566
|
-
expect(result).toEqual({});
|
|
567
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
568
|
-
expect.stringContaining("Step 'edit' in flow 'settingsOnlyFlow' has neither 'use' nor 'handler'"),
|
|
569
|
-
);
|
|
570
|
-
expect(errorSpy).not.toHaveBeenCalledWith(
|
|
571
|
-
expect.stringContaining("Step 'edit' in flow 'settingsOnlyFlow' has neither 'use' nor 'handler'"),
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
warnSpy.mockRestore();
|
|
575
|
-
errorSpy.mockRestore();
|
|
576
|
-
});
|
|
577
566
|
});
|
|
578
567
|
|
|
579
568
|
describe('beforeRender flows', () => {
|
|
@@ -796,7 +785,7 @@ describe('FlowModel', () => {
|
|
|
796
785
|
const eventFlow = createEventFlowDefinition('testEvent');
|
|
797
786
|
TestFlowModel.registerFlow(eventFlow);
|
|
798
787
|
|
|
799
|
-
const
|
|
788
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
800
789
|
|
|
801
790
|
try {
|
|
802
791
|
model.dispatchEvent('testEvent', { data: 'payload' });
|
|
@@ -804,7 +793,7 @@ describe('FlowModel', () => {
|
|
|
804
793
|
// Use a more reliable approach than arbitrary timeout
|
|
805
794
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
806
795
|
|
|
807
|
-
expect(
|
|
796
|
+
expect(loggerSpy).toHaveBeenCalledWith(
|
|
808
797
|
expect.stringContaining('[FlowModel] dispatchEvent: uid=test-model-uid, event=testEvent'),
|
|
809
798
|
);
|
|
810
799
|
expect(eventFlow.steps.eventStep.handler).toHaveBeenCalledWith(
|
|
@@ -814,7 +803,7 @@ describe('FlowModel', () => {
|
|
|
814
803
|
expect.any(Object),
|
|
815
804
|
);
|
|
816
805
|
} finally {
|
|
817
|
-
|
|
806
|
+
loggerSpy.mockRestore();
|
|
818
807
|
}
|
|
819
808
|
});
|
|
820
809
|
|
|
@@ -1625,7 +1614,7 @@ describe('FlowModel', () => {
|
|
|
1625
1614
|
fork1.dispose = vi.fn();
|
|
1626
1615
|
fork2.dispose = vi.fn();
|
|
1627
1616
|
|
|
1628
|
-
const
|
|
1617
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
1629
1618
|
|
|
1630
1619
|
try {
|
|
1631
1620
|
model.clearForks();
|
|
@@ -1634,19 +1623,19 @@ describe('FlowModel', () => {
|
|
|
1634
1623
|
expect(fork2.dispose).toHaveBeenCalled();
|
|
1635
1624
|
expect(model.forks.size).toBe(0);
|
|
1636
1625
|
} finally {
|
|
1637
|
-
|
|
1626
|
+
loggerSpy.mockRestore();
|
|
1638
1627
|
}
|
|
1639
1628
|
});
|
|
1640
1629
|
|
|
1641
1630
|
test('should handle empty forks collection when clearing', () => {
|
|
1642
|
-
const
|
|
1631
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
1643
1632
|
|
|
1644
1633
|
try {
|
|
1645
1634
|
model.clearForks();
|
|
1646
1635
|
|
|
1647
1636
|
expect(model.forks.size).toBe(0);
|
|
1648
1637
|
} finally {
|
|
1649
|
-
|
|
1638
|
+
loggerSpy.mockRestore();
|
|
1650
1639
|
}
|
|
1651
1640
|
});
|
|
1652
1641
|
});
|
|
@@ -1774,7 +1763,7 @@ describe('FlowModel', () => {
|
|
|
1774
1763
|
test('should clean up resources on remove', () => {
|
|
1775
1764
|
model.createFork();
|
|
1776
1765
|
model.createFork();
|
|
1777
|
-
const
|
|
1766
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
1778
1767
|
|
|
1779
1768
|
// Mock removeModel to simulate proper fork cleanup
|
|
1780
1769
|
flowEngine.removeModel = vi.fn().mockImplementation(() => {
|
|
@@ -1791,7 +1780,7 @@ describe('FlowModel', () => {
|
|
|
1791
1780
|
expect(model.forks.size).toBe(0);
|
|
1792
1781
|
expect(flowEngine.removeModel).toHaveBeenCalledWith(model.uid);
|
|
1793
1782
|
} finally {
|
|
1794
|
-
|
|
1783
|
+
loggerSpy.mockRestore();
|
|
1795
1784
|
}
|
|
1796
1785
|
});
|
|
1797
1786
|
});
|
|
@@ -1868,17 +1857,12 @@ describe('FlowModel', () => {
|
|
|
1868
1857
|
});
|
|
1869
1858
|
|
|
1870
1859
|
test('should rerender triggers beforeRender without cache', async () => {
|
|
1871
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1872
1860
|
model.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
|
|
1873
1861
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
});
|
|
1879
|
-
} finally {
|
|
1880
|
-
consoleSpy.mockRestore();
|
|
1881
|
-
}
|
|
1862
|
+
await expect(model.rerender()).resolves.not.toThrow();
|
|
1863
|
+
expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
|
|
1864
|
+
useCache: false,
|
|
1865
|
+
});
|
|
1882
1866
|
});
|
|
1883
1867
|
});
|
|
1884
1868
|
|
|
@@ -2918,7 +2902,7 @@ describe('FlowModel', () => {
|
|
|
2918
2902
|
describe('Edge Cases & Error Handling', () => {
|
|
2919
2903
|
test('should handle model destruction gracefully', () => {
|
|
2920
2904
|
const model = new FlowModel(modelOptions);
|
|
2921
|
-
const
|
|
2905
|
+
const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
|
|
2922
2906
|
|
|
2923
2907
|
model.createFork();
|
|
2924
2908
|
model.setProps({ testProp: 'value' });
|
|
@@ -2926,7 +2910,7 @@ describe('FlowModel', () => {
|
|
|
2926
2910
|
try {
|
|
2927
2911
|
expect(() => model.remove()).not.toThrow();
|
|
2928
2912
|
} finally {
|
|
2929
|
-
|
|
2913
|
+
loggerSpy.mockRestore();
|
|
2930
2914
|
}
|
|
2931
2915
|
});
|
|
2932
2916
|
|
package/src/models/flowModel.tsx
CHANGED
|
@@ -56,6 +56,25 @@ const classEventRegistries = new WeakMap<typeof FlowModel, ModelEventRegistry>()
|
|
|
56
56
|
// 使用WeakMap存储每个类的meta
|
|
57
57
|
const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
|
|
58
58
|
|
|
59
|
+
type SortableModelLike = {
|
|
60
|
+
sortIndex?: number | null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function getStableSortIndex(item: SortableModelLike, fallbackIndex: number) {
|
|
64
|
+
return typeof item?.sortIndex === 'number' && Number.isFinite(item.sortIndex) ? item.sortIndex : fallbackIndex + 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sortByStableSortIndex<T extends SortableModelLike>(items: T[]) {
|
|
68
|
+
return items
|
|
69
|
+
.map((item, index) => ({
|
|
70
|
+
item,
|
|
71
|
+
index,
|
|
72
|
+
sortIndex: getStableSortIndex(item, index),
|
|
73
|
+
}))
|
|
74
|
+
.sort((a, b) => a.sortIndex - b.sortIndex || a.index - b.index)
|
|
75
|
+
.map(({ item }) => item);
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
// 使用WeakMap存储每个类的 GlobalFlowRegistry
|
|
60
79
|
const modelGlobalRegistries = new WeakMap<typeof FlowModel, GlobalFlowRegistry>();
|
|
61
80
|
|
|
@@ -242,7 +261,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
242
261
|
};
|
|
243
262
|
this.stepParams = options.stepParams || {};
|
|
244
263
|
this.subModels = {};
|
|
245
|
-
this.sortIndex = options.sortIndex
|
|
264
|
+
this.sortIndex = getStableSortIndex({ sortIndex: options.sortIndex }, -1);
|
|
246
265
|
this._options = options;
|
|
247
266
|
this._title = '';
|
|
248
267
|
this._extraTitle = '';
|
|
@@ -511,11 +530,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
511
530
|
|
|
512
531
|
Object.entries(mergedSubModels || {}).forEach(([key, value]) => {
|
|
513
532
|
if (Array.isArray(value)) {
|
|
514
|
-
value
|
|
515
|
-
.
|
|
516
|
-
|
|
517
|
-
this.addSubModel(key, item);
|
|
518
|
-
});
|
|
533
|
+
sortByStableSortIndex(value).forEach((item) => {
|
|
534
|
+
this.addSubModel(key, item);
|
|
535
|
+
});
|
|
519
536
|
} else {
|
|
520
537
|
this.setSubModel(key, value);
|
|
521
538
|
}
|
|
@@ -778,26 +795,43 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
778
795
|
stepKeyOrStepsParams?: string | Record<string, ParamObject>,
|
|
779
796
|
params?: ParamObject,
|
|
780
797
|
): void {
|
|
798
|
+
let hasChanged = false;
|
|
799
|
+
|
|
781
800
|
if (typeof flowKeyOrAllParams === 'string') {
|
|
782
801
|
const flowKey = flowKeyOrAllParams;
|
|
783
802
|
if (typeof stepKeyOrStepsParams === 'string' && params !== undefined) {
|
|
784
|
-
|
|
785
|
-
|
|
803
|
+
const currentStepParams = this.stepParams[flowKey]?.[stepKeyOrStepsParams] || {};
|
|
804
|
+
const nextStepParams = { ...currentStepParams, ...params };
|
|
805
|
+
if (!_.isEqual(currentStepParams, nextStepParams)) {
|
|
806
|
+
if (!this.stepParams[flowKey]) {
|
|
807
|
+
this.stepParams[flowKey] = {};
|
|
808
|
+
}
|
|
809
|
+
this.stepParams[flowKey][stepKeyOrStepsParams] = nextStepParams;
|
|
810
|
+
hasChanged = true;
|
|
786
811
|
}
|
|
787
|
-
this.stepParams[flowKey][stepKeyOrStepsParams] = {
|
|
788
|
-
...this.stepParams[flowKey][stepKeyOrStepsParams],
|
|
789
|
-
...params,
|
|
790
|
-
};
|
|
791
812
|
} else if (typeof stepKeyOrStepsParams === 'object' && stepKeyOrStepsParams !== null) {
|
|
792
|
-
|
|
813
|
+
const currentFlowParams = this.stepParams[flowKey] || {};
|
|
814
|
+
const nextFlowParams = { ...currentFlowParams, ...stepKeyOrStepsParams };
|
|
815
|
+
if (!_.isEqual(currentFlowParams, nextFlowParams)) {
|
|
816
|
+
this.stepParams[flowKey] = nextFlowParams;
|
|
817
|
+
hasChanged = true;
|
|
818
|
+
}
|
|
793
819
|
}
|
|
794
820
|
} else if (typeof flowKeyOrAllParams === 'object' && flowKeyOrAllParams !== null) {
|
|
795
821
|
for (const fk in flowKeyOrAllParams) {
|
|
796
822
|
if (Object.prototype.hasOwnProperty.call(flowKeyOrAllParams, fk)) {
|
|
797
|
-
|
|
823
|
+
const currentFlowParams = this.stepParams[fk] || {};
|
|
824
|
+
const nextFlowParams = { ...currentFlowParams, ...flowKeyOrAllParams[fk] };
|
|
825
|
+
if (!_.isEqual(currentFlowParams, nextFlowParams)) {
|
|
826
|
+
this.stepParams[fk] = nextFlowParams;
|
|
827
|
+
hasChanged = true;
|
|
828
|
+
}
|
|
798
829
|
}
|
|
799
830
|
}
|
|
800
831
|
}
|
|
832
|
+
if (!hasChanged) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
801
835
|
// 发起配置修改事件
|
|
802
836
|
this.emitter.emit('onStepParamsChanged');
|
|
803
837
|
}
|
|
@@ -823,7 +857,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
823
857
|
}
|
|
824
858
|
const isFork = (this as any).isFork === true;
|
|
825
859
|
const target = this;
|
|
826
|
-
|
|
860
|
+
currentFlowEngine.logger.debug(
|
|
827
861
|
`[FlowModel] applyFlow: uid=${this.uid}, flowKey=${flowKey}, isFork=${isFork}, cleanRun=${
|
|
828
862
|
this.cleanRun
|
|
829
863
|
}, targetIsFork=${(target as any)?.isFork === true}`,
|
|
@@ -843,7 +877,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
843
877
|
}
|
|
844
878
|
const isFork = (this as any).isFork === true;
|
|
845
879
|
const target = this;
|
|
846
|
-
|
|
880
|
+
currentFlowEngine.logger.debug(
|
|
847
881
|
`[FlowModel] dispatchEvent: uid=${this.uid}, event=${eventName}, isFork=${isFork}, cleanRun=${
|
|
848
882
|
this.cleanRun
|
|
849
883
|
}, targetIsFork=${(target as any)?.isFork === true}`,
|
|
@@ -1206,7 +1240,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1206
1240
|
if (!Array.isArray(subModels[subKey])) {
|
|
1207
1241
|
subModels[subKey] = observable.shallow([]);
|
|
1208
1242
|
}
|
|
1209
|
-
const maxSortIndex = Math.max(
|
|
1243
|
+
const maxSortIndex = Math.max(
|
|
1244
|
+
...(subModels[subKey] as FlowModel[]).map((item, index) => getStableSortIndex(item, index)),
|
|
1245
|
+
0,
|
|
1246
|
+
);
|
|
1210
1247
|
model.sortIndex = maxSortIndex + 1;
|
|
1211
1248
|
subModels[subKey].push(model);
|
|
1212
1249
|
actualParent.emitter.emit('onSubModelAdded', model);
|
|
@@ -1264,14 +1301,12 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1264
1301
|
|
|
1265
1302
|
const results: ArrayElementType<NonNullable<Structure['subModels']>[K]>[] = [];
|
|
1266
1303
|
|
|
1267
|
-
_.castArray(model)
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
1304
|
+
sortByStableSortIndex(_.castArray(model)).forEach((item, index) => {
|
|
1305
|
+
const result = (callback as (model: any, index: number) => boolean)(item, index);
|
|
1306
|
+
if (result) {
|
|
1307
|
+
results.push(item);
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1275
1310
|
|
|
1276
1311
|
return results;
|
|
1277
1312
|
}
|
|
@@ -1288,12 +1323,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1288
1323
|
|
|
1289
1324
|
const results: R[] = [];
|
|
1290
1325
|
|
|
1291
|
-
_.castArray(model)
|
|
1292
|
-
|
|
1293
|
-
.
|
|
1294
|
-
|
|
1295
|
-
results.push(result);
|
|
1296
|
-
});
|
|
1326
|
+
sortByStableSortIndex(_.castArray(model)).forEach((item, index) => {
|
|
1327
|
+
const result = (callback as (model: any, index: number) => R)(item, index);
|
|
1328
|
+
results.push(result);
|
|
1329
|
+
});
|
|
1297
1330
|
|
|
1298
1331
|
return results;
|
|
1299
1332
|
}
|
|
@@ -1379,7 +1412,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1379
1412
|
}
|
|
1380
1413
|
|
|
1381
1414
|
clearForks() {
|
|
1382
|
-
|
|
1415
|
+
this.flowEngine.logger.debug(`FlowModel ${this.uid} clearing all forks.`);
|
|
1383
1416
|
// 主动使所有 fork 失效
|
|
1384
1417
|
if (this.forks?.size) {
|
|
1385
1418
|
this.forks.forEach((fork) => fork.dispose());
|