@nocobase/flow-engine 2.0.60 → 2.0.61

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 (39) hide show
  1. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -31
  2. package/lib/flowContext.d.ts +6 -1
  3. package/lib/flowContext.js +35 -6
  4. package/lib/flowEngine.d.ts +4 -3
  5. package/lib/flowEngine.js +67 -36
  6. package/lib/models/flowModel.js +45 -13
  7. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  8. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  9. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  10. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  11. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  12. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  13. package/lib/runjs-context/contexts/base.js +464 -29
  14. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  15. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  16. package/lib/utils/loadedPageCache.d.ts +21 -0
  17. package/lib/utils/loadedPageCache.js +125 -0
  18. package/package.json +4 -4
  19. package/src/__tests__/flowContext.test.ts +23 -0
  20. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  21. package/src/__tests__/runjsContext.test.ts +18 -0
  22. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  23. package/src/__tests__/runjsLocales.test.ts +6 -5
  24. package/src/__tests__/viewScopedFlowEngine.test.ts +133 -0
  25. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +79 -37
  26. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +148 -1
  27. package/src/flowContext.ts +40 -6
  28. package/src/flowEngine.ts +69 -34
  29. package/src/models/__tests__/flowModel.test.ts +13 -0
  30. package/src/models/flowModel.tsx +62 -29
  31. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  32. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  33. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  34. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  35. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  36. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  37. package/src/runjs-context/contexts/base.ts +467 -31
  38. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  39. package/src/utils/loadedPageCache.ts +117 -0
@@ -184,8 +184,17 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
184
184
  const [visible, setVisible] = (0, import_react.useState)(false);
185
185
  const [refreshTick, setRefreshTick] = (0, import_react.useState)(0);
186
186
  const [extraMenuItems, setExtraMenuItems] = (0, import_react.useState)([]);
187
+ const [extraMenuItemsLoaded, setExtraMenuItemsLoaded] = (0, import_react.useState)(false);
187
188
  const [configurableFlowsAndSteps, setConfigurableFlowsAndSteps] = (0, import_react.useState)([]);
188
189
  const [isLoading, setIsLoading] = (0, import_react.useState)(true);
190
+ const commonExtras = (0, import_react.useMemo)(
191
+ () => extraMenuItems.filter((it) => it.group === "common-actions").sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)),
192
+ [extraMenuItems]
193
+ );
194
+ const hasCommonActions = showCopyUidButton || showDeleteButton || commonExtras.length > 0;
195
+ const shouldDeferConfigLoading = flattenSubMenus && menuLevels > 1 && hasCommonActions;
196
+ const shouldWaitForCommonActionProbe = flattenSubMenus && menuLevels > 1 && !showCopyUidButton && !showDeleteButton && !extraMenuItemsLoaded;
197
+ const canRenderIcon = hasCommonActions || !isLoading && configurableFlowsAndSteps.length > 0;
189
198
  const closeDropdown = (0, import_react.useCallback)(() => {
190
199
  setVisible(false);
191
200
  onDropdownVisibleChange == null ? void 0 : onDropdownVisibleChange(false);
@@ -217,24 +226,28 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
217
226
  let mounted = true;
218
227
  const loadExtras = /* @__PURE__ */ __name(async () => {
219
228
  var _a;
220
- const allExtras = [];
221
- const modelsToProcess = [];
222
- walkSubModels(model, { maxDepth: menuLevels, arrayLimit: 50, mode: "stack" }, (targetModel, { modelKey }) => {
223
- modelsToProcess.push({ model: targetModel, modelKey });
224
- });
225
- for (const { model: targetModel, modelKey } of modelsToProcess) {
226
- const Cls = targetModel.constructor;
227
- const extras = await ((_a = Cls.getExtraMenuItems) == null ? void 0 : _a.call(Cls, targetModel, t));
228
- if (extras == null ? void 0 : extras.length) {
229
- allExtras.push(
230
- ...extras.map((item) => ({
231
- ...item,
232
- key: modelKey ? `${modelKey}:${item.key}` : item.key
233
- }))
234
- );
229
+ setExtraMenuItemsLoaded(false);
230
+ try {
231
+ const allExtras = [];
232
+ const modelsToProcess = [];
233
+ walkSubModels(model, { maxDepth: menuLevels, arrayLimit: 50, mode: "stack" }, (targetModel, { modelKey }) => {
234
+ modelsToProcess.push({ model: targetModel, modelKey });
235
+ });
236
+ for (const { model: targetModel, modelKey } of modelsToProcess) {
237
+ const Cls = targetModel.constructor;
238
+ const extras = await ((_a = Cls.getExtraMenuItems) == null ? void 0 : _a.call(Cls, targetModel, t));
239
+ if (extras == null ? void 0 : extras.length) {
240
+ allExtras.push(
241
+ ...extras.map((item) => ({
242
+ ...item,
243
+ key: modelKey ? `${modelKey}:${item.key}` : item.key
244
+ }))
245
+ );
246
+ }
247
+ }
248
+ if (!mounted) {
249
+ return;
235
250
  }
236
- }
237
- if (mounted) {
238
251
  const seen = /* @__PURE__ */ new Set();
239
252
  const dedupedExtras = allExtras.filter((item) => {
240
253
  if (seen.has(`${item.key}`)) {
@@ -244,15 +257,22 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
244
257
  return true;
245
258
  });
246
259
  setExtraMenuItems(dedupedExtras);
260
+ } catch (error) {
261
+ console.error("Failed to load extra menu items:", error);
262
+ if (mounted) {
263
+ setExtraMenuItems([]);
264
+ }
265
+ } finally {
266
+ if (mounted) {
267
+ setExtraMenuItemsLoaded(true);
268
+ }
247
269
  }
248
270
  }, "loadExtras");
249
- if (visible) {
250
- loadExtras();
251
- }
271
+ loadExtras();
252
272
  return () => {
253
273
  mounted = false;
254
274
  };
255
- }, [model, menuLevels, t, refreshTick, visible]);
275
+ }, [model, menuLevels, t, refreshTick]);
256
276
  const copyUidToClipboard = (0, import_react.useCallback)(
257
277
  async (uid) => {
258
278
  var _a;
@@ -494,7 +514,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
494
514
  return [];
495
515
  }
496
516
  },
497
- []
517
+ [t]
498
518
  );
499
519
  const getConfigurableFlowsAndSteps = (0, import_react.useCallback)(async () => {
500
520
  const result = [];
@@ -532,20 +552,47 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
532
552
  };
533
553
  }, [model, menuLevels, refreshTick]);
534
554
  (0, import_react.useEffect)(() => {
555
+ let mounted = true;
535
556
  const loadConfigurableFlowsAndSteps = /* @__PURE__ */ __name(async () => {
536
557
  setIsLoading(true);
558
+ if (shouldDeferConfigLoading) {
559
+ setConfigurableFlowsAndSteps([]);
560
+ }
537
561
  try {
538
562
  const flows = await getConfigurableFlowsAndSteps();
539
- setConfigurableFlowsAndSteps(flows);
563
+ if (mounted) {
564
+ setConfigurableFlowsAndSteps(flows);
565
+ }
540
566
  } catch (error) {
541
567
  console.error("Failed to load configurable flows and steps:", error);
542
- setConfigurableFlowsAndSteps([]);
568
+ if (mounted) {
569
+ setConfigurableFlowsAndSteps([]);
570
+ }
543
571
  } finally {
544
- setIsLoading(false);
572
+ if (mounted) {
573
+ setIsLoading(false);
574
+ }
545
575
  }
546
576
  }, "loadConfigurableFlowsAndSteps");
577
+ if (shouldWaitForCommonActionProbe) {
578
+ setConfigurableFlowsAndSteps([]);
579
+ setIsLoading(false);
580
+ return () => {
581
+ mounted = false;
582
+ };
583
+ }
584
+ if (!visible && shouldDeferConfigLoading) {
585
+ setConfigurableFlowsAndSteps([]);
586
+ setIsLoading(false);
587
+ return () => {
588
+ mounted = false;
589
+ };
590
+ }
547
591
  loadConfigurableFlowsAndSteps();
548
- }, [getConfigurableFlowsAndSteps, refreshTick]);
592
+ return () => {
593
+ mounted = false;
594
+ };
595
+ }, [getConfigurableFlowsAndSteps, refreshTick, shouldDeferConfigLoading, shouldWaitForCommonActionProbe, visible]);
549
596
  const menuItems = (0, import_react.useMemo)(() => {
550
597
  const items = [];
551
598
  const keyCounter = /* @__PURE__ */ new Map();
@@ -670,10 +717,9 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
670
717
  }
671
718
  }
672
719
  return items;
673
- }, [configurableFlowsAndSteps, disabledIconColor, flattenSubMenus, t]);
720
+ }, [configurableFlowsAndSteps, disabledIconColor, flattenSubMenus, message, model, t]);
674
721
  const finalMenuItems = (0, import_react.useMemo)(() => {
675
722
  const items = [...menuItems];
676
- const commonExtras = extraMenuItems.filter((it) => it.group === "common-actions").sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
677
723
  if (showCopyUidButton || showDeleteButton || commonExtras.length > 0) {
678
724
  items.push({
679
725
  type: "divider"
@@ -695,9 +741,8 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
695
741
  }
696
742
  }
697
743
  return items;
698
- }, [menuItems, showCopyUidButton, showDeleteButton, model.uid, model.destroy, t, extraMenuItems]);
699
- const hasExtras = extraMenuItems.some((it) => it.group === "common-actions");
700
- if (isLoading || configurableFlowsAndSteps.length === 0 && !showDeleteButton && !showCopyUidButton && !hasExtras) {
744
+ }, [menuItems, showCopyUidButton, showDeleteButton, commonExtras, model.uid, model.destroy, t]);
745
+ if (!canRenderIcon) {
701
746
  return null;
702
747
  }
703
748
  if (!model || !model.uid) {
@@ -222,6 +222,10 @@ export type FlowContextGetApiInfosOptions = {
222
222
  * RunJS 文档版本(默认 v1)。
223
223
  */
224
224
  version?: RunJSVersion;
225
+ /**
226
+ * Include editor completion metadata. Defaults to false so API-doc callers keep the compact public shape.
227
+ */
228
+ includeCompletion?: boolean;
225
229
  };
226
230
  export type FlowContextGetVarInfosOptions = {
227
231
  /**
@@ -293,7 +297,7 @@ export declare class FlowContext {
293
297
  * - 输出仅来自 RunJS doc 与 defineProperty/defineMethod 的 info
294
298
  * - 不读取/展开 PropertyMeta(变量结构)
295
299
  * - 不自动展开深层 properties
296
- * - 不返回自动补全字段(例如 completion
300
+ * - 默认不返回自动补全字段(例如 completion),传入 includeCompletion=true 时返回
297
301
  */
298
302
  getApiInfos(options?: FlowContextGetApiInfosOptions): Promise<Record<string, FlowContextApiInfo>>;
299
303
  /**
@@ -421,6 +425,7 @@ export declare class FlowRuntimeContext<TModel extends FlowModel = FlowModel, TM
421
425
  export type FlowSettingsContext<TModel extends FlowModel = FlowModel> = FlowRuntimeContext<TModel, 'settings'>;
422
426
  export type RunJSDocCompletionDoc = {
423
427
  insertText?: string;
428
+ requires?: Array<'element'>;
424
429
  };
425
430
  export type RunJSDocHiddenDoc = boolean | ((ctx: any) => boolean | Promise<boolean>);
426
431
  export type RunJSDocHiddenOrPathsDoc = boolean | string[] | ((ctx: any) => boolean | string[] | Promise<boolean | string[]>);
@@ -399,10 +399,11 @@ const _FlowContext = class _FlowContext {
399
399
  * - 输出仅来自 RunJS doc 与 defineProperty/defineMethod 的 info
400
400
  * - 不读取/展开 PropertyMeta(变量结构)
401
401
  * - 不自动展开深层 properties
402
- * - 不返回自动补全字段(例如 completion
402
+ * - 默认不返回自动补全字段(例如 completion),传入 includeCompletion=true 时返回
403
403
  */
404
404
  async getApiInfos(options = {}) {
405
405
  const version = options.version || "v1";
406
+ const includeCompletion = !!options.includeCompletion;
406
407
  const evalCtx = this.createProxy();
407
408
  const isPrivateKey = /* @__PURE__ */ __name((key) => typeof key === "string" && key.startsWith("_"), "isPrivateKey");
408
409
  const isVarRootKey = /* @__PURE__ */ __name((key) => key === "record" || key === "formValues" || key === "popup", "isVarRootKey");
@@ -439,7 +440,14 @@ const _FlowContext = class _FlowContext {
439
440
  const src = toDocObject(obj);
440
441
  if (!src) return {};
441
442
  const out2 = {};
442
- for (const k of ["description", "examples", "ref", "params", "returns"]) {
443
+ for (const k of [
444
+ "description",
445
+ "examples",
446
+ ...includeCompletion ? ["completion"] : [],
447
+ "ref",
448
+ "params",
449
+ "returns"
450
+ ]) {
443
451
  const v = src[k];
444
452
  if (typeof v !== "undefined") out2[k] = v;
445
453
  }
@@ -452,7 +460,17 @@ const _FlowContext = class _FlowContext {
452
460
  const src = toDocObject(obj);
453
461
  if (!src) return {};
454
462
  const out2 = {};
455
- for (const k of ["title", "type", "interface", "description", "examples", "ref", "params", "returns"]) {
463
+ for (const k of [
464
+ "title",
465
+ "type",
466
+ "interface",
467
+ "description",
468
+ "examples",
469
+ ...includeCompletion ? ["completion"] : [],
470
+ "ref",
471
+ "params",
472
+ "returns"
473
+ ]) {
456
474
  const v = src[k];
457
475
  if (typeof v !== "undefined") out2[k] = v;
458
476
  }
@@ -539,7 +557,7 @@ const _FlowContext = class _FlowContext {
539
557
  node = { ...node, ...pickPropertyInfo(docObj) };
540
558
  node = { ...node, ...pickPropertyInfo(infoObj) };
541
559
  delete node.properties;
542
- delete node.completion;
560
+ if (!includeCompletion) delete node.completion;
543
561
  if (!Object.keys(node).length) continue;
544
562
  const outKey = mapDocKeyToApiKey(key, docNode);
545
563
  out[outKey] = out[outKey] ? { ...out[outKey] || {}, ...node || {} } : node;
@@ -554,7 +572,7 @@ const _FlowContext = class _FlowContext {
554
572
  node = { ...node, ...pickMethodInfo(docObj) };
555
573
  node = { ...node, ...pickMethodInfo(info) };
556
574
  delete node.properties;
557
- delete node.completion;
575
+ if (!includeCompletion) delete node.completion;
558
576
  if (!Object.keys(node).length) continue;
559
577
  node.type = "function";
560
578
  if (!out[key]) out[key] = node;
@@ -571,7 +589,7 @@ const _FlowContext = class _FlowContext {
571
589
  let node = {};
572
590
  node = { ...node, ...pickPropertyInfo(childObj) };
573
591
  delete node.properties;
574
- delete node.completion;
592
+ if (!includeCompletion) delete node.completion;
575
593
  if (!node.description || !String(node.description).trim()) continue;
576
594
  out[outKey] = node;
577
595
  }
@@ -2255,6 +2273,17 @@ const _BaseFlowEngineContext = class _BaseFlowEngineContext extends FlowContext
2255
2273
  });
2256
2274
  const jsCode = await (0, import_utils.prepareRunJsCode)(String(code ?? ""), { preprocessTemplates: shouldPreprocessTemplates });
2257
2275
  return runner.run(jsCode);
2276
+ },
2277
+ {
2278
+ description: "Execute a RunJS code string in the current Flow context.",
2279
+ detail: "(code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<RunJSResult>",
2280
+ params: [
2281
+ { name: "code", type: "string", description: "RunJS code to execute." },
2282
+ { name: "variables", type: "Record<string, any>", optional: true, description: "Additional globals." },
2283
+ { name: "options", type: "JSRunnerOptions", optional: true, description: "Runner options." }
2284
+ ],
2285
+ returns: { type: "Promise<{ success: boolean; value?: any; error?: any; timeout?: boolean }>" },
2286
+ completion: { insertText: `await ctx.runjs('return 1')` }
2258
2287
  }
2259
2288
  );
2260
2289
  }
@@ -71,6 +71,7 @@ export declare class FlowEngine {
71
71
  * @private
72
72
  */
73
73
  private _savingModels;
74
+ private _loadedPageCache;
74
75
  /**
75
76
  * Flow engine context object.
76
77
  * @private
@@ -406,11 +407,11 @@ export declare class FlowEngine {
406
407
  replaceModel<T extends FlowModel = FlowModel>(uid: string, optionsOrFn?: Partial<FlowModelOptions> | ((currentOptions: FlowModelOptions) => Partial<FlowModelOptions>)): Promise<T | null>;
407
408
  /**
408
409
  * Move a model instance within its parent model.
409
- * @param {any} sourceId Source model UID
410
- * @param {any} targetId Target model UID
410
+ * @param {string | number} sourceId Source model UID
411
+ * @param {string | number} targetId Target model UID
411
412
  * @returns {Promise<void>} No return value
412
413
  */
413
- moveModel(sourceId: any, targetId: any, options?: PersistOptions): Promise<void>;
414
+ moveModel(sourceId: string | number, targetId: string | number, options?: PersistOptions): Promise<void>;
414
415
  /**
415
416
  * Filter model classes by parent class (supports multi-level inheritance).
416
417
  * @param {string | ModelConstructor} parentClass Parent class name or constructor
package/lib/flowEngine.js CHANGED
@@ -61,6 +61,7 @@ var import_ReactView = require("./ReactView");
61
61
  var import_resources = require("./resources");
62
62
  var import_emitter = require("./emitter");
63
63
  var import_ModelOperationScheduler = __toESM(require("./scheduler/ModelOperationScheduler"));
64
+ var import_loadedPageCache = require("./utils/loadedPageCache");
64
65
  var import_utils = require("./utils");
65
66
  var _FlowEngine_instances, registerModel_fn;
66
67
  const getFlowEngineLoggerLevel = /* @__PURE__ */ __name(() => process.env.NODE_ENV === "production" ? "warn" : "trace", "getFlowEngineLoggerLevel");
@@ -108,6 +109,7 @@ const _FlowEngine = class _FlowEngine {
108
109
  * @private
109
110
  */
110
111
  __publicField(this, "_savingModels", /* @__PURE__ */ new Map());
112
+ __publicField(this, "_loadedPageCache", (0, import_loadedPageCache.createLoadedPageCache)());
111
113
  /**
112
114
  * Flow engine context object.
113
115
  * @private
@@ -789,10 +791,11 @@ const _FlowEngine = class _FlowEngine {
789
791
  async loadModel(options) {
790
792
  if (!this.ensureModelRepository()) return;
791
793
  const refresh = !!(options == null ? void 0 : options.refresh);
792
- if (!refresh) {
793
- const model = this.findModelByParentId(options.parentId, options.subKey);
794
- if (model) {
795
- return model;
794
+ const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
795
+ if (!refresh && !bypassLoadedPageCache) {
796
+ const model2 = this.findModelByParentId(options.parentId, options.subKey);
797
+ if (model2) {
798
+ return model2;
796
799
  }
797
800
  const hydrated = this.hydrateModelFromPreviousEngines(options);
798
801
  if (hydrated) {
@@ -800,14 +803,24 @@ const _FlowEngine = class _FlowEngine {
800
803
  }
801
804
  }
802
805
  const data = await this._modelRepository.findOne(options);
803
- if (!(data == null ? void 0 : data.uid)) return null;
804
- if (refresh) {
806
+ if (!(data == null ? void 0 : data.uid)) {
807
+ if (bypassLoadedPageCache) {
808
+ this._loadedPageCache.clear(options);
809
+ }
810
+ return null;
811
+ }
812
+ if (refresh || bypassLoadedPageCache) {
805
813
  const existing = this.getModel(data.uid);
806
814
  if (existing) {
807
815
  this.removeModelWithSubModels(existing.uid);
808
816
  }
809
817
  }
810
- return this.createModel(data);
818
+ const model = this.createModel(data);
819
+ if (bypassLoadedPageCache) {
820
+ this._loadedPageCache.mountModelToParent(model, true);
821
+ this._loadedPageCache.clear(options);
822
+ }
823
+ return model;
811
824
  }
812
825
  /**
813
826
  * Find a sub-model by parent model ID and subKey.
@@ -838,20 +851,29 @@ const _FlowEngine = class _FlowEngine {
838
851
  async loadOrCreateModel(options, extra) {
839
852
  if (!this.ensureModelRepository()) return;
840
853
  const { uid, parentId, subKey } = options;
841
- if (uid && this._modelInstances.has(uid)) {
854
+ const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
855
+ if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
842
856
  return this._modelInstances.get(uid);
843
857
  }
844
- const m = this.findModelByParentId(parentId, subKey);
845
- if (m) {
846
- return m;
847
- }
848
- const hydrated = this.hydrateModelFromPreviousEngines(options, extra);
849
- if (hydrated) {
850
- return hydrated;
858
+ if (!bypassLoadedPageCache) {
859
+ const m = this.findModelByParentId(parentId, subKey);
860
+ if (m) {
861
+ return m;
862
+ }
863
+ const hydrated = this.hydrateModelFromPreviousEngines(options, extra);
864
+ if (hydrated) {
865
+ return hydrated;
866
+ }
851
867
  }
852
868
  const data = await this._modelRepository.findOne(options);
853
869
  let model = null;
854
870
  if (data == null ? void 0 : data.uid) {
871
+ if (bypassLoadedPageCache) {
872
+ const existing = this.getModel(data.uid);
873
+ if (existing) {
874
+ this.removeModelWithSubModels(existing.uid);
875
+ }
876
+ }
855
877
  model = this.createModel(data, extra);
856
878
  } else {
857
879
  model = this.createModel(options, extra);
@@ -859,18 +881,9 @@ const _FlowEngine = class _FlowEngine {
859
881
  await model.save();
860
882
  }
861
883
  }
862
- if (model.parent) {
863
- const subModel = model.parent.findSubModel(model.subKey, (m2) => {
864
- return m2.uid === model.uid;
865
- });
866
- if (subModel) {
867
- return model;
868
- }
869
- if (model.subType === "array") {
870
- model.parent.addSubModel(model.subKey, model);
871
- } else {
872
- model.parent.setSubModel(model.subKey, model);
873
- }
884
+ this._loadedPageCache.mountModelToParent(model, bypassLoadedPageCache);
885
+ if (bypassLoadedPageCache) {
886
+ this._loadedPageCache.clear(options);
874
887
  }
875
888
  return model;
876
889
  }
@@ -888,6 +901,7 @@ const _FlowEngine = class _FlowEngine {
888
901
  async saveModel(model, options) {
889
902
  if (!this.ensureModelRepository()) return;
890
903
  const modelUid = model.uid;
904
+ const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(model);
891
905
  if (this._savingModels.has(modelUid)) {
892
906
  this.logger.debug(`Model ${modelUid} is already being saved, waiting for existing save operation`);
893
907
  return await this._savingModels.get(modelUid);
@@ -896,6 +910,7 @@ const _FlowEngine = class _FlowEngine {
896
910
  this._savingModels.set(modelUid, savePromise);
897
911
  try {
898
912
  const result = await savePromise;
913
+ this._loadedPageCache.markDirty(dirtyLoadedPageKey);
899
914
  return result;
900
915
  } finally {
901
916
  this._savingModels.delete(modelUid);
@@ -926,10 +941,15 @@ const _FlowEngine = class _FlowEngine {
926
941
  * @returns {Promise<boolean>} Whether destroyed successfully
927
942
  */
928
943
  async destroyModel(uid) {
929
- if (this.ensureModelRepository()) {
944
+ const modelInstance = this._modelInstances.get(uid);
945
+ const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(modelInstance);
946
+ const hasModelRepository = this.ensureModelRepository();
947
+ if (hasModelRepository) {
930
948
  await this._modelRepository.destroy(uid);
931
949
  }
932
- const modelInstance = this._modelInstances.get(uid);
950
+ if (hasModelRepository) {
951
+ this._loadedPageCache.markDirty(dirtyLoadedPageKey);
952
+ }
933
953
  const parent = modelInstance == null ? void 0 : modelInstance.parent;
934
954
  const result = this.removeModel(uid);
935
955
  parent && parent.emitter.emit("onSubModelDestroyed", modelInstance);
@@ -1005,18 +1025,25 @@ const _FlowEngine = class _FlowEngine {
1005
1025
  }
1006
1026
  /**
1007
1027
  * Move a model instance within its parent model.
1008
- * @param {any} sourceId Source model UID
1009
- * @param {any} targetId Target model UID
1028
+ * @param {string | number} sourceId Source model UID
1029
+ * @param {string | number} targetId Target model UID
1010
1030
  * @returns {Promise<void>} No return value
1011
1031
  */
1012
1032
  async moveModel(sourceId, targetId, options) {
1013
1033
  var _a, _b;
1014
- const sourceModel = this.getModel(sourceId);
1015
- const targetModel = this.getModel(targetId);
1034
+ const sourceUid = String(sourceId);
1035
+ const targetUid = String(targetId);
1036
+ if (!sourceUid || !targetUid || sourceUid === targetUid) {
1037
+ return;
1038
+ }
1039
+ const sourceModel = this.getModel(sourceUid);
1040
+ const targetModel = this.getModel(targetUid);
1016
1041
  if (!sourceModel || !targetModel) {
1017
1042
  console.warn(`FlowEngine: Cannot move model. Source or target model not found.`);
1018
1043
  return;
1019
1044
  }
1045
+ let position = "after";
1046
+ const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(sourceModel);
1020
1047
  const move = /* @__PURE__ */ __name((sourceModel2, targetModel2) => {
1021
1048
  if (!sourceModel2.parent || !targetModel2.parent || sourceModel2.parent !== targetModel2.parent) {
1022
1049
  console.error("FlowModel.moveTo: Both models must have the same parent to perform move operation.");
@@ -1039,6 +1066,7 @@ const _FlowEngine = class _FlowEngine {
1039
1066
  console.warn("FlowModel.moveTo: Current model is already at the target position. No action taken.");
1040
1067
  return false;
1041
1068
  }
1069
+ position = currentIndex < targetIndex ? "after" : "before";
1042
1070
  const [movedModel] = subModelsCopy.splice(currentIndex, 1);
1043
1071
  subModelsCopy.splice(targetIndex, 0, movedModel);
1044
1072
  subModelsCopy.forEach((model, index) => {
@@ -1047,10 +1075,13 @@ const _FlowEngine = class _FlowEngine {
1047
1075
  subModels.splice(0, subModels.length, ...subModelsCopy);
1048
1076
  return true;
1049
1077
  }, "move");
1050
- move(sourceModel, targetModel);
1078
+ const moved = move(sourceModel, targetModel);
1079
+ if (!moved) {
1080
+ return;
1081
+ }
1051
1082
  if ((options == null ? void 0 : options.persist) !== false && this.ensureModelRepository()) {
1052
- const position = sourceModel.sortIndex - targetModel.sortIndex > 0 ? "after" : "before";
1053
- await this._modelRepository.move(sourceId, targetId, position);
1083
+ await this._modelRepository.move(sourceUid, targetUid, position);
1084
+ this._loadedPageCache.markDirty(dirtyLoadedPageKey);
1054
1085
  }
1055
1086
  sourceModel.parent.emitter.emit("onSubModelMoved", { source: sourceModel, target: targetModel });
1056
1087
  (_b = this.emitter) == null ? void 0 : _b.emit("model:subModel:moved", {
@@ -73,6 +73,18 @@ var _flowContext;
73
73
  const classActionRegistries = /* @__PURE__ */ new WeakMap();
74
74
  const classEventRegistries = /* @__PURE__ */ new WeakMap();
75
75
  const modelMetas = /* @__PURE__ */ new WeakMap();
76
+ function getStableSortIndex(item, fallbackIndex) {
77
+ return typeof (item == null ? void 0 : item.sortIndex) === "number" && Number.isFinite(item.sortIndex) ? item.sortIndex : fallbackIndex + 1;
78
+ }
79
+ __name(getStableSortIndex, "getStableSortIndex");
80
+ function sortByStableSortIndex(items) {
81
+ return items.map((item, index) => ({
82
+ item,
83
+ index,
84
+ sortIndex: getStableSortIndex(item, index)
85
+ })).sort((a, b) => a.sortIndex - b.sortIndex || a.index - b.index).map(({ item }) => item);
86
+ }
87
+ __name(sortByStableSortIndex, "sortByStableSortIndex");
76
88
  const modelGlobalRegistries = /* @__PURE__ */ new WeakMap();
77
89
  const classMenuExtensions = /* @__PURE__ */ new WeakMap();
78
90
  var ModelRenderMode = /* @__PURE__ */ ((ModelRenderMode2) => {
@@ -161,7 +173,7 @@ const _FlowModel = class _FlowModel {
161
173
  };
162
174
  this.stepParams = options.stepParams || {};
163
175
  this.subModels = {};
164
- this.sortIndex = options.sortIndex || 0;
176
+ this.sortIndex = getStableSortIndex({ sortIndex: options.sortIndex }, -1);
165
177
  this._options = options;
166
178
  this._title = "";
167
179
  this._extraTitle = "";
@@ -373,7 +385,7 @@ const _FlowModel = class _FlowModel {
373
385
  }
374
386
  Object.entries(mergedSubModels || {}).forEach(([key, value]) => {
375
387
  if (Array.isArray(value)) {
376
- value.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0)).forEach((item) => {
388
+ sortByStableSortIndex(value).forEach((item) => {
377
389
  this.addSubModel(key, item);
378
390
  });
379
391
  } else {
@@ -584,26 +596,43 @@ const _FlowModel = class _FlowModel {
584
596
  return this.props;
585
597
  }
586
598
  setStepParams(flowKeyOrAllParams, stepKeyOrStepsParams, params) {
599
+ var _a;
600
+ let hasChanged = false;
587
601
  if (typeof flowKeyOrAllParams === "string") {
588
602
  const flowKey = flowKeyOrAllParams;
589
603
  if (typeof stepKeyOrStepsParams === "string" && params !== void 0) {
590
- if (!this.stepParams[flowKey]) {
591
- this.stepParams[flowKey] = {};
604
+ const currentStepParams = ((_a = this.stepParams[flowKey]) == null ? void 0 : _a[stepKeyOrStepsParams]) || {};
605
+ const nextStepParams = { ...currentStepParams, ...params };
606
+ if (!import_lodash.default.isEqual(currentStepParams, nextStepParams)) {
607
+ if (!this.stepParams[flowKey]) {
608
+ this.stepParams[flowKey] = {};
609
+ }
610
+ this.stepParams[flowKey][stepKeyOrStepsParams] = nextStepParams;
611
+ hasChanged = true;
592
612
  }
593
- this.stepParams[flowKey][stepKeyOrStepsParams] = {
594
- ...this.stepParams[flowKey][stepKeyOrStepsParams],
595
- ...params
596
- };
597
613
  } else if (typeof stepKeyOrStepsParams === "object" && stepKeyOrStepsParams !== null) {
598
- this.stepParams[flowKey] = { ...this.stepParams[flowKey] || {}, ...stepKeyOrStepsParams };
614
+ const currentFlowParams = this.stepParams[flowKey] || {};
615
+ const nextFlowParams = { ...currentFlowParams, ...stepKeyOrStepsParams };
616
+ if (!import_lodash.default.isEqual(currentFlowParams, nextFlowParams)) {
617
+ this.stepParams[flowKey] = nextFlowParams;
618
+ hasChanged = true;
619
+ }
599
620
  }
600
621
  } else if (typeof flowKeyOrAllParams === "object" && flowKeyOrAllParams !== null) {
601
622
  for (const fk in flowKeyOrAllParams) {
602
623
  if (Object.prototype.hasOwnProperty.call(flowKeyOrAllParams, fk)) {
603
- this.stepParams[fk] = { ...this.stepParams[fk] || {}, ...flowKeyOrAllParams[fk] };
624
+ const currentFlowParams = this.stepParams[fk] || {};
625
+ const nextFlowParams = { ...currentFlowParams, ...flowKeyOrAllParams[fk] };
626
+ if (!import_lodash.default.isEqual(currentFlowParams, nextFlowParams)) {
627
+ this.stepParams[fk] = nextFlowParams;
628
+ hasChanged = true;
629
+ }
604
630
  }
605
631
  }
606
632
  }
633
+ if (!hasChanged) {
634
+ return;
635
+ }
607
636
  this.emitter.emit("onStepParamsChanged");
608
637
  }
609
638
  getStepParams(flowKey, stepKey) {
@@ -889,7 +918,10 @@ const _FlowModel = class _FlowModel {
889
918
  if (!Array.isArray(subModels[subKey])) {
890
919
  subModels[subKey] = import_reactive.observable.shallow([]);
891
920
  }
892
- const maxSortIndex = Math.max(...subModels[subKey].map((item) => item.sortIndex || 0), 0);
921
+ const maxSortIndex = Math.max(
922
+ ...subModels[subKey].map((item, index) => getStableSortIndex(item, index)),
923
+ 0
924
+ );
893
925
  model.sortIndex = maxSortIndex + 1;
894
926
  subModels[subKey].push(model);
895
927
  actualParent.emitter.emit("onSubModelAdded", model);
@@ -937,7 +969,7 @@ const _FlowModel = class _FlowModel {
937
969
  return [];
938
970
  }
939
971
  const results = [];
940
- import_lodash.default.castArray(model).sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0)).forEach((item, index) => {
972
+ sortByStableSortIndex(import_lodash.default.castArray(model)).forEach((item, index) => {
941
973
  const result = callback(item, index);
942
974
  if (result) {
943
975
  results.push(item);
@@ -951,7 +983,7 @@ const _FlowModel = class _FlowModel {
951
983
  return [];
952
984
  }
953
985
  const results = [];
954
- import_lodash.default.castArray(model).sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0)).forEach((item, index) => {
986
+ sortByStableSortIndex(import_lodash.default.castArray(model)).forEach((item, index) => {
955
987
  const result = callback(item, index);
956
988
  results.push(result);
957
989
  });