@nocobase/flow-engine 2.1.0-beta.42 → 2.1.0-beta.44
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/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -31
- package/lib/components/subModel/LazyDropdown.js +17 -9
- package/lib/executor/FlowExecutor.js +0 -3
- package/lib/flowContext.d.ts +6 -1
- package/lib/flowContext.js +35 -6
- package/lib/flowEngine.d.ts +4 -3
- package/lib/flowEngine.js +69 -37
- package/lib/models/flowModel.js +45 -13
- 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/package.json +4 -4
- package/src/__tests__/flowContext.test.ts +23 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
- 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 +79 -37
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +150 -3
- package/src/components/subModel/LazyDropdown.tsx +16 -7
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +51 -0
- package/src/executor/FlowExecutor.ts +0 -3
- package/src/executor/__tests__/flowExecutor.test.ts +2 -4
- package/src/flowContext.ts +40 -6
- package/src/flowEngine.ts +71 -35
- package/src/models/__tests__/flowModel.test.ts +13 -28
- package/src/models/flowModel.tsx +62 -29
- 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/loadedPageCache.ts +147 -0
package/src/flowContext.ts
CHANGED
|
@@ -400,6 +400,10 @@ export type FlowContextGetApiInfosOptions = {
|
|
|
400
400
|
* RunJS 文档版本(默认 v1)。
|
|
401
401
|
*/
|
|
402
402
|
version?: RunJSVersion;
|
|
403
|
+
/**
|
|
404
|
+
* Include editor completion metadata. Defaults to false so API-doc callers keep the compact public shape.
|
|
405
|
+
*/
|
|
406
|
+
includeCompletion?: boolean;
|
|
403
407
|
};
|
|
404
408
|
|
|
405
409
|
export type FlowContextGetVarInfosOptions = {
|
|
@@ -704,10 +708,11 @@ export class FlowContext {
|
|
|
704
708
|
* - 输出仅来自 RunJS doc 与 defineProperty/defineMethod 的 info
|
|
705
709
|
* - 不读取/展开 PropertyMeta(变量结构)
|
|
706
710
|
* - 不自动展开深层 properties
|
|
707
|
-
* -
|
|
711
|
+
* - 默认不返回自动补全字段(例如 completion),传入 includeCompletion=true 时返回
|
|
708
712
|
*/
|
|
709
713
|
async getApiInfos(options: FlowContextGetApiInfosOptions = {}): Promise<Record<string, FlowContextApiInfo>> {
|
|
710
714
|
const version = (options.version as RunJSVersion) || ('v1' as RunJSVersion);
|
|
715
|
+
const includeCompletion = !!options.includeCompletion;
|
|
711
716
|
const evalCtx = this.createProxy();
|
|
712
717
|
|
|
713
718
|
const isPrivateKey = (key: string) => typeof key === 'string' && key.startsWith('_');
|
|
@@ -759,7 +764,14 @@ export class FlowContext {
|
|
|
759
764
|
const src = toDocObject(obj);
|
|
760
765
|
if (!src) return {};
|
|
761
766
|
const out: any = {};
|
|
762
|
-
for (const k of [
|
|
767
|
+
for (const k of [
|
|
768
|
+
'description',
|
|
769
|
+
'examples',
|
|
770
|
+
...(includeCompletion ? ['completion'] : []),
|
|
771
|
+
'ref',
|
|
772
|
+
'params',
|
|
773
|
+
'returns',
|
|
774
|
+
]) {
|
|
763
775
|
const v = (src as any)[k];
|
|
764
776
|
if (typeof v !== 'undefined') out[k] = v;
|
|
765
777
|
}
|
|
@@ -773,7 +785,17 @@ export class FlowContext {
|
|
|
773
785
|
const src = toDocObject(obj);
|
|
774
786
|
if (!src) return {};
|
|
775
787
|
const out: any = {};
|
|
776
|
-
for (const k of [
|
|
788
|
+
for (const k of [
|
|
789
|
+
'title',
|
|
790
|
+
'type',
|
|
791
|
+
'interface',
|
|
792
|
+
'description',
|
|
793
|
+
'examples',
|
|
794
|
+
...(includeCompletion ? ['completion'] : []),
|
|
795
|
+
'ref',
|
|
796
|
+
'params',
|
|
797
|
+
'returns',
|
|
798
|
+
]) {
|
|
777
799
|
const v = (src as any)[k];
|
|
778
800
|
if (typeof v !== 'undefined') out[k] = v;
|
|
779
801
|
}
|
|
@@ -872,7 +894,7 @@ export class FlowContext {
|
|
|
872
894
|
node = { ...node, ...pickPropertyInfo(docObj) };
|
|
873
895
|
node = { ...node, ...pickPropertyInfo(infoObj) };
|
|
874
896
|
delete (node as any).properties;
|
|
875
|
-
delete (node as any).completion;
|
|
897
|
+
if (!includeCompletion) delete (node as any).completion;
|
|
876
898
|
if (!Object.keys(node).length) continue;
|
|
877
899
|
const outKey = mapDocKeyToApiKey(key, docNode);
|
|
878
900
|
// Avoid exposing ctx.React/ctx.ReactDOM/ctx.antd in api docs when mapping to ctx.libs.*.
|
|
@@ -890,7 +912,7 @@ export class FlowContext {
|
|
|
890
912
|
node = { ...node, ...pickMethodInfo(docObj) };
|
|
891
913
|
node = { ...node, ...pickMethodInfo(info) };
|
|
892
914
|
delete (node as any).properties;
|
|
893
|
-
delete (node as any).completion;
|
|
915
|
+
if (!includeCompletion) delete (node as any).completion;
|
|
894
916
|
if (!Object.keys(node).length) continue;
|
|
895
917
|
node.type = 'function';
|
|
896
918
|
|
|
@@ -913,7 +935,7 @@ export class FlowContext {
|
|
|
913
935
|
let node: FlowContextApiInfo = {};
|
|
914
936
|
node = { ...node, ...pickPropertyInfo(childObj) };
|
|
915
937
|
delete (node as any).properties;
|
|
916
|
-
delete (node as any).completion;
|
|
938
|
+
if (!includeCompletion) delete (node as any).completion;
|
|
917
939
|
if (!node.description || !String(node.description).trim()) continue;
|
|
918
940
|
out[outKey] = node;
|
|
919
941
|
}
|
|
@@ -3075,6 +3097,17 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
3075
3097
|
const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
|
|
3076
3098
|
return runner.run(jsCode);
|
|
3077
3099
|
},
|
|
3100
|
+
{
|
|
3101
|
+
description: 'Execute a RunJS code string in the current Flow context.',
|
|
3102
|
+
detail: '(code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<RunJSResult>',
|
|
3103
|
+
params: [
|
|
3104
|
+
{ name: 'code', type: 'string', description: 'RunJS code to execute.' },
|
|
3105
|
+
{ name: 'variables', type: 'Record<string, any>', optional: true, description: 'Additional globals.' },
|
|
3106
|
+
{ name: 'options', type: 'JSRunnerOptions', optional: true, description: 'Runner options.' },
|
|
3107
|
+
],
|
|
3108
|
+
returns: { type: 'Promise<{ success: boolean; value?: any; error?: any; timeout?: boolean }>' },
|
|
3109
|
+
completion: { insertText: `await ctx.runjs('return 1')` },
|
|
3110
|
+
},
|
|
3078
3111
|
);
|
|
3079
3112
|
}
|
|
3080
3113
|
}
|
|
@@ -3927,6 +3960,7 @@ export type FlowSettingsContext<TModel extends FlowModel = FlowModel> = FlowRunt
|
|
|
3927
3960
|
|
|
3928
3961
|
export type RunJSDocCompletionDoc = {
|
|
3929
3962
|
insertText?: string;
|
|
3963
|
+
requires?: Array<'element'>;
|
|
3930
3964
|
};
|
|
3931
3965
|
|
|
3932
3966
|
export type RunJSDocHiddenDoc = boolean | ((ctx: any) => boolean | Promise<boolean>);
|
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,
|
|
@@ -135,6 +136,8 @@ export class FlowEngine {
|
|
|
135
136
|
*/
|
|
136
137
|
private _savingModels = new Map<string, Promise<any>>();
|
|
137
138
|
|
|
139
|
+
private _loadedPageCache = createLoadedPageCache();
|
|
140
|
+
|
|
138
141
|
/**
|
|
139
142
|
* Flow engine context object.
|
|
140
143
|
* @private
|
|
@@ -1339,7 +1342,8 @@ export class FlowEngine {
|
|
|
1339
1342
|
async loadModel<T extends FlowModel = FlowModel>(options): Promise<T | null> {
|
|
1340
1343
|
if (!this.ensureModelRepository()) return;
|
|
1341
1344
|
const refresh = !!options?.refresh;
|
|
1342
|
-
|
|
1345
|
+
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1346
|
+
if (!refresh && !bypassLoadedPageCache) {
|
|
1343
1347
|
const model = this.findModelByParentId(options.parentId, options.subKey);
|
|
1344
1348
|
if (model) {
|
|
1345
1349
|
return model as T;
|
|
@@ -1350,15 +1354,24 @@ export class FlowEngine {
|
|
|
1350
1354
|
}
|
|
1351
1355
|
}
|
|
1352
1356
|
const data = await this._modelRepository.findOne(options);
|
|
1353
|
-
if (!data?.uid)
|
|
1354
|
-
|
|
1355
|
-
|
|
1357
|
+
if (!data?.uid) {
|
|
1358
|
+
if (bypassLoadedPageCache) {
|
|
1359
|
+
this._loadedPageCache.clear(options);
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
if (refresh || bypassLoadedPageCache) {
|
|
1356
1364
|
const existing = this.getModel(data.uid);
|
|
1357
1365
|
if (existing) {
|
|
1358
1366
|
this.removeModelWithSubModels(existing.uid);
|
|
1359
1367
|
}
|
|
1360
1368
|
}
|
|
1361
|
-
|
|
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;
|
|
1362
1375
|
}
|
|
1363
1376
|
|
|
1364
1377
|
/**
|
|
@@ -1398,22 +1411,31 @@ export class FlowEngine {
|
|
|
1398
1411
|
): Promise<T | null> {
|
|
1399
1412
|
if (!this.ensureModelRepository()) return;
|
|
1400
1413
|
const { uid, parentId, subKey } = options;
|
|
1401
|
-
|
|
1414
|
+
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1415
|
+
if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
|
|
1402
1416
|
return this._modelInstances.get(uid) as T;
|
|
1403
1417
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1418
|
+
if (!bypassLoadedPageCache) {
|
|
1419
|
+
const m = this.findModelByParentId<T>(parentId, subKey);
|
|
1420
|
+
if (m) {
|
|
1421
|
+
return m;
|
|
1422
|
+
}
|
|
1408
1423
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1424
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1425
|
+
if (hydrated) {
|
|
1426
|
+
return hydrated;
|
|
1427
|
+
}
|
|
1412
1428
|
}
|
|
1413
1429
|
|
|
1414
1430
|
const data = await this._modelRepository.findOne(options);
|
|
1415
1431
|
let model: T | null = null;
|
|
1416
1432
|
if (data?.uid) {
|
|
1433
|
+
if (bypassLoadedPageCache) {
|
|
1434
|
+
const existing = this.getModel(data.uid);
|
|
1435
|
+
if (existing) {
|
|
1436
|
+
this.removeModelWithSubModels(existing.uid);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1417
1439
|
model = await this.createModelAsync<T>(data as any, extra);
|
|
1418
1440
|
} else {
|
|
1419
1441
|
model = await this.createModelAsync<T>(options, extra);
|
|
@@ -1421,18 +1443,9 @@ export class FlowEngine {
|
|
|
1421
1443
|
await model.save();
|
|
1422
1444
|
}
|
|
1423
1445
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
});
|
|
1428
|
-
if (subModel) {
|
|
1429
|
-
return model;
|
|
1430
|
-
}
|
|
1431
|
-
if (model.subType === 'array') {
|
|
1432
|
-
model.parent.addSubModel(model.subKey, model);
|
|
1433
|
-
} else {
|
|
1434
|
-
model.parent.setSubModel(model.subKey, model);
|
|
1435
|
-
}
|
|
1446
|
+
this._loadedPageCache.mountModelToParent(model, bypassLoadedPageCache);
|
|
1447
|
+
if (bypassLoadedPageCache) {
|
|
1448
|
+
this._loadedPageCache.clear(options);
|
|
1436
1449
|
}
|
|
1437
1450
|
return model;
|
|
1438
1451
|
}
|
|
@@ -1452,6 +1465,9 @@ export class FlowEngine {
|
|
|
1452
1465
|
if (!this.ensureModelRepository()) return;
|
|
1453
1466
|
|
|
1454
1467
|
const modelUid = model.uid;
|
|
1468
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(model, {
|
|
1469
|
+
force: !!options?.onlyStepParams,
|
|
1470
|
+
});
|
|
1455
1471
|
|
|
1456
1472
|
// 如果这个 model 正在保存中,返回现有的保存 Promise
|
|
1457
1473
|
if (this._savingModels.has(modelUid)) {
|
|
@@ -1465,6 +1481,7 @@ export class FlowEngine {
|
|
|
1465
1481
|
|
|
1466
1482
|
try {
|
|
1467
1483
|
const result = await savePromise;
|
|
1484
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1468
1485
|
return result;
|
|
1469
1486
|
} finally {
|
|
1470
1487
|
// 无论成功还是失败,都要清除保存状态
|
|
@@ -1501,11 +1518,16 @@ export class FlowEngine {
|
|
|
1501
1518
|
* @returns {Promise<boolean>} Whether destroyed successfully
|
|
1502
1519
|
*/
|
|
1503
1520
|
async destroyModel(uid: string) {
|
|
1504
|
-
|
|
1521
|
+
const modelInstance = this._modelInstances.get(uid) as FlowModel;
|
|
1522
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(modelInstance);
|
|
1523
|
+
const hasModelRepository = this.ensureModelRepository();
|
|
1524
|
+
if (hasModelRepository) {
|
|
1505
1525
|
await this._modelRepository.destroy(uid);
|
|
1506
1526
|
}
|
|
1507
1527
|
|
|
1508
|
-
|
|
1528
|
+
if (hasModelRepository) {
|
|
1529
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1530
|
+
}
|
|
1509
1531
|
const parent = modelInstance?.parent;
|
|
1510
1532
|
const result = this.removeModel(uid);
|
|
1511
1533
|
parent && parent.emitter.emit('onSubModelDestroyed', modelInstance);
|
|
@@ -1609,17 +1631,25 @@ export class FlowEngine {
|
|
|
1609
1631
|
|
|
1610
1632
|
/**
|
|
1611
1633
|
* Move a model instance within its parent model.
|
|
1612
|
-
* @param {
|
|
1613
|
-
* @param {
|
|
1634
|
+
* @param {string | number} sourceId Source model UID
|
|
1635
|
+
* @param {string | number} targetId Target model UID
|
|
1614
1636
|
* @returns {Promise<void>} No return value
|
|
1615
1637
|
*/
|
|
1616
|
-
async moveModel(sourceId:
|
|
1617
|
-
const
|
|
1618
|
-
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);
|
|
1619
1647
|
if (!sourceModel || !targetModel) {
|
|
1620
1648
|
console.warn(`FlowEngine: Cannot move model. Source or target model not found.`);
|
|
1621
1649
|
return;
|
|
1622
1650
|
}
|
|
1651
|
+
let position: 'before' | 'after' = 'after';
|
|
1652
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(sourceModel);
|
|
1623
1653
|
const move = (sourceModel: FlowModel, targetModel: FlowModel) => {
|
|
1624
1654
|
if (!sourceModel.parent || !targetModel.parent || sourceModel.parent !== targetModel.parent) {
|
|
1625
1655
|
console.error('FlowModel.moveTo: Both models must have the same parent to perform move operation.');
|
|
@@ -1649,6 +1679,8 @@ export class FlowEngine {
|
|
|
1649
1679
|
return false;
|
|
1650
1680
|
}
|
|
1651
1681
|
|
|
1682
|
+
position = currentIndex < targetIndex ? 'after' : 'before';
|
|
1683
|
+
|
|
1652
1684
|
// 使用splice直接移动数组元素(O(n)比排序O(n log n)更快)
|
|
1653
1685
|
const [movedModel] = subModelsCopy.splice(currentIndex, 1);
|
|
1654
1686
|
subModelsCopy.splice(targetIndex, 0, movedModel);
|
|
@@ -1663,10 +1695,14 @@ export class FlowEngine {
|
|
|
1663
1695
|
|
|
1664
1696
|
return true;
|
|
1665
1697
|
};
|
|
1666
|
-
move(sourceModel, targetModel);
|
|
1698
|
+
const moved = move(sourceModel, targetModel);
|
|
1699
|
+
if (!moved) {
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1667
1703
|
if (options?.persist !== false && this.ensureModelRepository()) {
|
|
1668
|
-
|
|
1669
|
-
|
|
1704
|
+
await this._modelRepository.move(sourceUid, targetUid, position);
|
|
1705
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1670
1706
|
}
|
|
1671
1707
|
// 触发事件以通知其他部分模型已移动
|
|
1672
1708
|
sourceModel.parent.emitter.emit('onSubModelMoved', { source: sourceModel, target: targetModel });
|
|
@@ -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
|
|
|
@@ -550,34 +563,6 @@ describe('FlowModel', () => {
|
|
|
550
563
|
|
|
551
564
|
loggerSpy.mockRestore();
|
|
552
565
|
});
|
|
553
|
-
|
|
554
|
-
test('should warn and skip step when use and handler are both missing', async () => {
|
|
555
|
-
const warnSpy = vi.spyOn(model.context.logger, 'warn').mockImplementation(() => {});
|
|
556
|
-
const errorSpy = vi.spyOn(model.context.logger, 'error').mockImplementation(() => {});
|
|
557
|
-
|
|
558
|
-
TestFlowModel.registerFlow({
|
|
559
|
-
key: 'settingsOnlyFlow',
|
|
560
|
-
steps: {
|
|
561
|
-
edit: {
|
|
562
|
-
title: 'Edit',
|
|
563
|
-
uiSchema: {},
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const result = await model.applyFlow('settingsOnlyFlow');
|
|
569
|
-
|
|
570
|
-
expect(result).toEqual({});
|
|
571
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
572
|
-
expect.stringContaining("Step 'edit' in flow 'settingsOnlyFlow' has neither 'use' nor 'handler'"),
|
|
573
|
-
);
|
|
574
|
-
expect(errorSpy).not.toHaveBeenCalledWith(
|
|
575
|
-
expect.stringContaining("Step 'edit' in flow 'settingsOnlyFlow' has neither 'use' nor 'handler'"),
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
warnSpy.mockRestore();
|
|
579
|
-
errorSpy.mockRestore();
|
|
580
|
-
});
|
|
581
566
|
});
|
|
582
567
|
|
|
583
568
|
describe('beforeRender flows', () => {
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowRunJSContext } from '../../flowContext';
|
|
11
|
+
import { createElementPropertyDoc, createZhCNElementPropertyDoc } from './elementDoc';
|
|
11
12
|
|
|
12
13
|
export class FormJSFieldItemRunJSContext extends FlowRunJSContext {}
|
|
13
14
|
|
|
14
15
|
FormJSFieldItemRunJSContext.define({
|
|
15
16
|
label: 'FormJSFieldItem RunJS context',
|
|
16
17
|
properties: {
|
|
17
|
-
element: `ElementProxy instance providing a safe DOM container for form field rendering.
|
|
18
|
-
Supports innerHTML, append, and other DOM manipulation methods
|
|
18
|
+
element: createElementPropertyDoc(`ElementProxy instance providing a safe DOM container for form field rendering.
|
|
19
|
+
Supports innerHTML, append, and other DOM manipulation methods.`),
|
|
19
20
|
value: `Current field value (read-only in display mode; in controlled scenarios, use setProps to modify).`,
|
|
20
21
|
record: `Current record data object (read-only).
|
|
21
22
|
Contains all field values of the parent record.`,
|
|
@@ -38,7 +39,7 @@ FormJSFieldItemRunJSContext.define(
|
|
|
38
39
|
{
|
|
39
40
|
label: '表单 JS 字段项 RunJS 上下文',
|
|
40
41
|
properties: {
|
|
41
|
-
element: 'ElementProxy,表单字段容器',
|
|
42
|
+
element: createZhCNElementPropertyDoc('ElementProxy,表单字段容器'),
|
|
42
43
|
value: '字段值(展示模式为只读;受控场景用 setProps 修改)',
|
|
43
44
|
record: '当前记录(只读)',
|
|
44
45
|
formValues: {
|
|
@@ -8,21 +8,16 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowRunJSContext } from '../../flowContext';
|
|
11
|
+
import { createElementPropertyDoc, createZhCNElementPropertyDoc } from './elementDoc';
|
|
11
12
|
|
|
12
13
|
export class JSBlockRunJSContext extends FlowRunJSContext {}
|
|
13
14
|
|
|
14
15
|
JSBlockRunJSContext.define({
|
|
15
16
|
label: 'RunJS context',
|
|
16
17
|
properties: {
|
|
17
|
-
element:
|
|
18
|
-
description: `ElementProxy instance providing a safe DOM container.
|
|
18
|
+
element: createElementPropertyDoc(`ElementProxy instance providing a safe DOM container.
|
|
19
19
|
Supports innerHTML, append, and other DOM manipulation methods.
|
|
20
|
-
Use this to render content in the JS block
|
|
21
|
-
detail: 'ElementProxy',
|
|
22
|
-
properties: {
|
|
23
|
-
innerHTML: 'Set or read the HTML content of the container element.',
|
|
24
|
-
},
|
|
25
|
-
},
|
|
20
|
+
Use this to render content in the JS block.`),
|
|
26
21
|
record: `Current record data object (read-only).
|
|
27
22
|
Available when the JS block is within a data block or detail view context.`,
|
|
28
23
|
value: 'Current value of the field or component, if available in the current context.',
|
|
@@ -44,13 +39,7 @@ JSBlockRunJSContext.define(
|
|
|
44
39
|
{
|
|
45
40
|
label: 'RunJS 上下文',
|
|
46
41
|
properties: {
|
|
47
|
-
element:
|
|
48
|
-
description: 'ElementProxy,安全的 DOM 容器,支持 innerHTML/append 等',
|
|
49
|
-
detail: 'ElementProxy',
|
|
50
|
-
properties: {
|
|
51
|
-
innerHTML: '读取或设置容器的 HTML 内容',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
42
|
+
element: createZhCNElementPropertyDoc('ElementProxy,安全的 DOM 容器,支持 innerHTML/append 等'),
|
|
54
43
|
record: '当前记录(只读,用于数据区块/详情等场景)',
|
|
55
44
|
value: '当前值(若存在)',
|
|
56
45
|
React: 'React 库',
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowRunJSContext } from '../../flowContext';
|
|
11
|
+
import { createElementPropertyDoc, createZhCNElementPropertyDoc } from './elementDoc';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* RunJS context for JSColumnModel (table custom column).
|
|
@@ -18,8 +19,9 @@ export class JSColumnRunJSContext extends FlowRunJSContext {}
|
|
|
18
19
|
JSColumnRunJSContext.define({
|
|
19
20
|
label: 'JSColumn RunJS context',
|
|
20
21
|
properties: {
|
|
21
|
-
element:
|
|
22
|
+
element: createElementPropertyDoc(
|
|
22
23
|
'ElementProxy instance providing a safe DOM container for the current table cell. Supports innerHTML/append and basic DOM APIs.',
|
|
24
|
+
),
|
|
23
25
|
record: 'Current row record object (read-only).',
|
|
24
26
|
recordIndex: 'Index of the current row in the page (0-based).',
|
|
25
27
|
collection: 'Collection definition metadata (read-only).',
|
|
@@ -40,7 +42,7 @@ JSColumnRunJSContext.define(
|
|
|
40
42
|
{
|
|
41
43
|
label: 'JS 列 RunJS 上下文',
|
|
42
44
|
properties: {
|
|
43
|
-
element: 'ElementProxy,表格单元格的安全 DOM 容器,支持 innerHTML/append 等',
|
|
45
|
+
element: createZhCNElementPropertyDoc('ElementProxy,表格单元格的安全 DOM 容器,支持 innerHTML/append 等'),
|
|
44
46
|
record: '当前行记录对象(只读)',
|
|
45
47
|
recordIndex: '当前行索引(从 0 开始)',
|
|
46
48
|
collection: '集合定义元数据(只读)',
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowRunJSContext } from '../../flowContext';
|
|
11
|
+
import { createElementPropertyDoc, createZhCNElementPropertyDoc } from './elementDoc';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* RunJS context for JSEditableFieldModel (form editable custom field).
|
|
@@ -19,11 +20,9 @@ export class JSEditableFieldRunJSContext extends FlowRunJSContext {}
|
|
|
19
20
|
JSEditableFieldRunJSContext.define({
|
|
20
21
|
label: 'JSEditableField RunJS context',
|
|
21
22
|
properties: {
|
|
22
|
-
element:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
detail: 'ElementProxy',
|
|
26
|
-
},
|
|
23
|
+
element: createElementPropertyDoc(
|
|
24
|
+
'ElementProxy instance providing a safe DOM container for field rendering. In editable mode this container is typically a <span> element.',
|
|
25
|
+
),
|
|
27
26
|
value: {
|
|
28
27
|
description:
|
|
29
28
|
'Current field value (read-only snapshot). In editable scenarios, prefer ctx.getValue()/ctx.setValue(v) for two-way binding.',
|
|
@@ -69,10 +68,7 @@ JSEditableFieldRunJSContext.define(
|
|
|
69
68
|
{
|
|
70
69
|
label: 'JS 可编辑字段 RunJS 上下文',
|
|
71
70
|
properties: {
|
|
72
|
-
element:
|
|
73
|
-
description: 'ElementProxy,字段渲染的安全容器(通常为 <span> 容器)。',
|
|
74
|
-
detail: 'ElementProxy',
|
|
75
|
-
},
|
|
71
|
+
element: createZhCNElementPropertyDoc('ElementProxy,字段渲染的安全容器(通常为 <span> 容器)。'),
|
|
76
72
|
value: {
|
|
77
73
|
description: '字段当前值(只读快照)。可编辑场景建议使用 ctx.getValue()/ctx.setValue(v) 做双向绑定。',
|
|
78
74
|
detail: 'any',
|