@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.
Files changed (45) hide show
  1. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -31
  2. package/lib/components/subModel/LazyDropdown.js +17 -9
  3. package/lib/executor/FlowExecutor.js +0 -3
  4. package/lib/flowContext.d.ts +6 -1
  5. package/lib/flowContext.js +35 -6
  6. package/lib/flowEngine.d.ts +4 -3
  7. package/lib/flowEngine.js +69 -37
  8. package/lib/models/flowModel.js +45 -13
  9. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  10. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  11. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  12. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  13. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  14. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  15. package/lib/runjs-context/contexts/base.js +464 -29
  16. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  17. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  18. package/lib/utils/loadedPageCache.d.ts +24 -0
  19. package/lib/utils/loadedPageCache.js +139 -0
  20. package/package.json +4 -4
  21. package/src/__tests__/flowContext.test.ts +23 -0
  22. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  23. package/src/__tests__/runjsContext.test.ts +18 -0
  24. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  25. package/src/__tests__/runjsLocales.test.ts +6 -5
  26. package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
  27. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +79 -37
  28. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +150 -3
  29. package/src/components/subModel/LazyDropdown.tsx +16 -7
  30. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +51 -0
  31. package/src/executor/FlowExecutor.ts +0 -3
  32. package/src/executor/__tests__/flowExecutor.test.ts +2 -4
  33. package/src/flowContext.ts +40 -6
  34. package/src/flowEngine.ts +71 -35
  35. package/src/models/__tests__/flowModel.test.ts +13 -28
  36. package/src/models/flowModel.tsx +62 -29
  37. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  38. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  39. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  40. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  41. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  42. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  43. package/src/runjs-context/contexts/base.ts +467 -31
  44. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  45. package/src/utils/loadedPageCache.ts +147 -0
@@ -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
- * - 不返回自动补全字段(例如 completion
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 ['description', 'examples', 'ref', 'params', 'returns']) {
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 ['title', 'type', 'interface', 'description', 'examples', 'ref', 'params', 'returns']) {
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
- if (!refresh) {
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) return null;
1354
- await this.resolveModelTree(data);
1355
- if (refresh) {
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
- return this.createModelAsync<T>(data as any);
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
- if (uid && this._modelInstances.has(uid)) {
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
- const m = this.findModelByParentId<T>(parentId, subKey);
1405
- if (m) {
1406
- return m;
1407
- }
1418
+ if (!bypassLoadedPageCache) {
1419
+ const m = this.findModelByParentId<T>(parentId, subKey);
1420
+ if (m) {
1421
+ return m;
1422
+ }
1408
1423
 
1409
- const hydrated = await this.hydrateModelFromPreviousEngines<T>(options, extra);
1410
- if (hydrated) {
1411
- return hydrated;
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
- if (model.parent) {
1425
- const subModel = model.parent.findSubModel(model.subKey, (m) => {
1426
- return m.uid === model.uid;
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
- if (this.ensureModelRepository()) {
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
- const modelInstance = this._modelInstances.get(uid) as FlowModel;
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 {any} sourceId Source model UID
1613
- * @param {any} targetId Target model UID
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: any, targetId: any, options?: PersistOptions): Promise<void> {
1617
- const sourceModel = this.getModel(sourceId);
1618
- const targetModel = this.getModel(targetId);
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
- const position = sourceModel.sortIndex - targetModel.sortIndex > 0 ? 'after' : 'before';
1669
- await this._modelRepository.move(sourceId, targetId, position);
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', () => {
@@ -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 || 0;
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
- .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
516
- .forEach((item) => {
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
- if (!this.stepParams[flowKey]) {
785
- this.stepParams[flowKey] = {};
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
- this.stepParams[flowKey] = { ...(this.stepParams[flowKey] || {}), ...stepKeyOrStepsParams };
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
- this.stepParams[fk] = { ...(this.stepParams[fk] || {}), ...flowKeyOrAllParams[fk] };
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(...(subModels[subKey] as FlowModel[]).map((item) => item.sortIndex || 0), 0);
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
- .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
1269
- .forEach((item, index) => {
1270
- const result = (callback as (model: any, index: number) => boolean)(item, index);
1271
- if (result) {
1272
- results.push(item);
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
- .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
1293
- .forEach((item, index) => {
1294
- const result = (callback as (model: any, index: number) => R)(item, index);
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
- description:
24
- 'ElementProxy instance providing a safe DOM container for field rendering. In editable mode this container is typically a <span> element.',
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',