@nocobase/flow-engine 2.1.0-beta.9 → 2.1.0

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 (215) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/FieldModelRenderer.js +2 -2
  4. package/lib/components/FlowModelRenderer.d.ts +3 -1
  5. package/lib/components/FlowModelRenderer.js +12 -6
  6. package/lib/components/FormItem.d.ts +6 -0
  7. package/lib/components/FormItem.js +11 -3
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  10. package/lib/components/dnd/gridDragPlanner.js +607 -19
  11. package/lib/components/dnd/index.d.ts +31 -2
  12. package/lib/components/dnd/index.js +244 -23
  13. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  14. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  15. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
  17. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  18. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  19. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  20. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  21. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  22. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  23. package/lib/components/subModel/AddSubModelButton.js +12 -1
  24. package/lib/components/subModel/LazyDropdown.js +301 -52
  25. package/lib/components/subModel/index.d.ts +1 -0
  26. package/lib/components/subModel/index.js +19 -0
  27. package/lib/components/subModel/utils.d.ts +2 -1
  28. package/lib/components/subModel/utils.js +15 -5
  29. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  30. package/lib/components/variables/VariableHybridInput.js +499 -0
  31. package/lib/components/variables/index.d.ts +2 -0
  32. package/lib/components/variables/index.js +3 -0
  33. package/lib/data-source/index.d.ts +84 -0
  34. package/lib/data-source/index.js +269 -7
  35. package/lib/executor/FlowExecutor.js +6 -3
  36. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  37. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  38. package/lib/flow-registry/index.d.ts +1 -0
  39. package/lib/flow-registry/index.js +3 -1
  40. package/lib/flowContext.d.ts +9 -1
  41. package/lib/flowContext.js +77 -6
  42. package/lib/flowEngine.d.ts +136 -4
  43. package/lib/flowEngine.js +429 -51
  44. package/lib/flowI18n.js +2 -1
  45. package/lib/flowSettings.d.ts +14 -6
  46. package/lib/flowSettings.js +34 -6
  47. package/lib/index.d.ts +2 -0
  48. package/lib/index.js +7 -0
  49. package/lib/lazy-helper.d.ts +14 -0
  50. package/lib/lazy-helper.js +71 -0
  51. package/lib/locale/en-US.json +1 -0
  52. package/lib/locale/index.d.ts +2 -0
  53. package/lib/locale/zh-CN.json +1 -0
  54. package/lib/models/DisplayItemModel.d.ts +1 -1
  55. package/lib/models/EditableItemModel.d.ts +1 -1
  56. package/lib/models/FilterableItemModel.d.ts +1 -1
  57. package/lib/models/flowModel.d.ts +13 -10
  58. package/lib/models/flowModel.js +126 -34
  59. package/lib/provider.js +38 -23
  60. package/lib/reactive/observer.js +46 -16
  61. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  62. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  63. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  64. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  65. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  66. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  67. package/lib/runjs-context/contexts/base.js +464 -29
  68. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  69. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  70. package/lib/runjs-context/setup.js +1 -0
  71. package/lib/runjs-context/snippets/index.js +13 -2
  72. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  74. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  75. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/loadedPageCache.d.ts +24 -0
  82. package/lib/utils/loadedPageCache.js +139 -0
  83. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  84. package/lib/utils/parsePathnameToViewParams.js +28 -4
  85. package/lib/utils/randomId.d.ts +39 -0
  86. package/lib/utils/randomId.js +45 -0
  87. package/lib/utils/runjsTemplateCompat.js +1 -1
  88. package/lib/utils/runjsValue.js +41 -11
  89. package/lib/utils/schema-utils.d.ts +7 -1
  90. package/lib/utils/schema-utils.js +19 -0
  91. package/lib/views/FlowView.d.ts +7 -1
  92. package/lib/views/FlowView.js +11 -1
  93. package/lib/views/PageComponent.js +8 -6
  94. package/lib/views/ViewNavigation.d.ts +12 -2
  95. package/lib/views/ViewNavigation.js +28 -9
  96. package/lib/views/createViewMeta.js +114 -50
  97. package/lib/views/inheritLayoutContext.d.ts +10 -0
  98. package/lib/views/inheritLayoutContext.js +50 -0
  99. package/lib/views/runViewBeforeClose.d.ts +10 -0
  100. package/lib/views/runViewBeforeClose.js +45 -0
  101. package/lib/views/useDialog.d.ts +2 -1
  102. package/lib/views/useDialog.js +12 -3
  103. package/lib/views/useDrawer.d.ts +2 -1
  104. package/lib/views/useDrawer.js +12 -3
  105. package/lib/views/usePage.d.ts +5 -11
  106. package/lib/views/usePage.js +304 -144
  107. package/package.json +5 -4
  108. package/src/FlowContextProvider.tsx +9 -1
  109. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  110. package/src/__tests__/flow-engine.test.ts +166 -0
  111. package/src/__tests__/flowContext.test.ts +105 -1
  112. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  113. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +21 -0
  120. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  121. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  122. package/src/__tests__/runjsLocales.test.ts +6 -5
  123. package/src/__tests__/runjsSnippets.test.ts +21 -0
  124. package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
  125. package/src/components/FieldModelRenderer.tsx +2 -1
  126. package/src/components/FlowModelRenderer.tsx +18 -6
  127. package/src/components/FormItem.tsx +7 -1
  128. package/src/components/MobilePopup.tsx +4 -2
  129. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  130. package/src/components/__tests__/FormItem.test.tsx +25 -0
  131. package/src/components/__tests__/dnd.test.ts +44 -0
  132. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  133. package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
  134. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  135. package/src/components/dnd/gridDragPlanner.ts +750 -17
  136. package/src/components/dnd/index.tsx +305 -28
  137. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  138. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
  139. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +16 -2
  145. package/src/components/subModel/LazyDropdown.tsx +341 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +13 -2
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +69 -2
  154. package/src/data-source/index.ts +332 -8
  155. package/src/executor/FlowExecutor.ts +6 -3
  156. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +85 -6
  161. package/src/flowEngine.ts +484 -45
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  172. package/src/models/__tests__/flowModel.test.ts +65 -37
  173. package/src/models/flowModel.tsx +184 -65
  174. package/src/provider.tsx +41 -25
  175. package/src/reactive/__tests__/observer.test.tsx +82 -0
  176. package/src/reactive/observer.tsx +87 -25
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  179. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  180. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  181. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  182. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  183. package/src/runjs-context/contexts/base.ts +467 -31
  184. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  185. package/src/runjs-context/setup.ts +1 -0
  186. package/src/runjs-context/snippets/index.ts +12 -1
  187. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  188. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  189. package/src/types.ts +62 -0
  190. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  191. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  192. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  193. package/src/utils/__tests__/utils.test.ts +62 -0
  194. package/src/utils/createCollectionContextMeta.ts +6 -2
  195. package/src/utils/index.ts +5 -1
  196. package/src/utils/loadedPageCache.ts +147 -0
  197. package/src/utils/parsePathnameToViewParams.ts +45 -5
  198. package/src/utils/randomId.ts +48 -0
  199. package/src/utils/runjsTemplateCompat.ts +1 -1
  200. package/src/utils/runjsValue.ts +50 -11
  201. package/src/utils/schema-utils.ts +30 -1
  202. package/src/views/FlowView.tsx +22 -2
  203. package/src/views/PageComponent.tsx +7 -4
  204. package/src/views/ViewNavigation.ts +46 -9
  205. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  206. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  207. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  208. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  209. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
  210. package/src/views/createViewMeta.ts +106 -34
  211. package/src/views/inheritLayoutContext.ts +26 -0
  212. package/src/views/runViewBeforeClose.ts +19 -0
  213. package/src/views/useDialog.tsx +13 -3
  214. package/src/views/useDrawer.tsx +13 -3
  215. package/src/views/usePage.tsx +367 -180
package/lib/flowEngine.js CHANGED
@@ -61,8 +61,10 @@ 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;
67
+ const getFlowEngineLoggerLevel = /* @__PURE__ */ __name(() => process.env.NODE_ENV === "production" ? "warn" : "trace", "getFlowEngineLoggerLevel");
66
68
  const _FlowEngine = class _FlowEngine {
67
69
  /**
68
70
  * Constructor. Initializes React view, registers default model and form scopes.
@@ -83,6 +85,28 @@ const _FlowEngine = class _FlowEngine {
83
85
  * @private
84
86
  */
85
87
  __publicField(this, "_modelClasses", import_reactive.observable.shallow(/* @__PURE__ */ new Map()));
88
+ /**
89
+ * Registered model entries.
90
+ * Key is the model class name, value is the model loader entry.
91
+ * @private
92
+ */
93
+ __publicField(this, "_modelLoaders", /* @__PURE__ */ new Map());
94
+ /**
95
+ * In-flight model loading promises.
96
+ * Key is the model class name, value is the loading promise.
97
+ * @private
98
+ */
99
+ __publicField(this, "_loadingModelPromises", /* @__PURE__ */ new Map());
100
+ /**
101
+ * Whether model-loader preload has completed in this session.
102
+ * @private
103
+ */
104
+ __publicField(this, "_modelLoadersPreloaded", false);
105
+ /**
106
+ * In-flight model-loader preload promise.
107
+ * @private
108
+ */
109
+ __publicField(this, "_modelLoadersPreloadPromise");
86
110
  /**
87
111
  * Created model instances.
88
112
  * Key is the model instance UID, value is the model instance object.
@@ -107,6 +131,7 @@ const _FlowEngine = class _FlowEngine {
107
131
  * @private
108
132
  */
109
133
  __publicField(this, "_savingModels", /* @__PURE__ */ new Map());
134
+ __publicField(this, "_loadedPageCache", (0, import_loadedPageCache.createLoadedPageCache)());
110
135
  /**
111
136
  * Flow engine context object.
112
137
  * @private
@@ -173,7 +198,7 @@ const _FlowEngine = class _FlowEngine {
173
198
  MultiRecordResource: import_resources.MultiRecordResource
174
199
  });
175
200
  this.logger = (0, import_pino.default)({
176
- level: "trace",
201
+ level: getFlowEngineLoggerLevel(),
177
202
  browser: {
178
203
  write: {
179
204
  fatal: /* @__PURE__ */ __name((o) => console.trace(o), "fatal"),
@@ -372,6 +397,16 @@ const _FlowEngine = class _FlowEngine {
372
397
  getEvents() {
373
398
  return this._eventRegistry.getEvents();
374
399
  }
400
+ /**
401
+ * for proxy instance, the #registerModel can't be called.
402
+ */
403
+ _registerModel(name, modelClass) {
404
+ if (this._modelClasses.has(name)) {
405
+ console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
406
+ }
407
+ Object.defineProperty(modelClass, "name", { value: name });
408
+ this._modelClasses.set(name, modelClass);
409
+ }
375
410
  /**
376
411
  * Register multiple model classes.
377
412
  * @param {Record<string, ModelConstructor>} models Model class map, key is model name, value is model constructor
@@ -384,6 +419,274 @@ const _FlowEngine = class _FlowEngine {
384
419
  __privateMethod(this, _FlowEngine_instances, registerModel_fn).call(this, name, modelClass);
385
420
  }
386
421
  }
422
+ /**
423
+ * Register multiple model loader entries.
424
+ * The `extends` field declares parent class(es) for async subclass discovery via `getSubclassesOfAsync`.
425
+ * It accepts `string | ModelConstructor | (string | ModelConstructor)[]` and is normalized to `string[]` internally.
426
+ * @param {FlowModelLoaderInputMap} loaders Model loader input map
427
+ * @returns {void}
428
+ * @example
429
+ * flowEngine.registerModelLoaders({
430
+ * DemoModel: {
431
+ * extends: 'BaseModel',
432
+ * loader: () => import('./models/DemoModel'),
433
+ * },
434
+ * });
435
+ */
436
+ registerModelLoaders(loaders) {
437
+ let changed = false;
438
+ for (const [name, input] of Object.entries(loaders)) {
439
+ if (this._modelLoaders.has(name)) {
440
+ console.warn(`FlowEngine: Model loader with name '${name}' is already registered and will be overwritten.`);
441
+ }
442
+ const entry = {
443
+ loader: input.loader
444
+ };
445
+ if (input.extends != null) {
446
+ const raw = Array.isArray(input.extends) ? input.extends : [input.extends];
447
+ entry.extends = raw.map((item) => typeof item === "string" ? item : item.name);
448
+ }
449
+ this._modelLoaders.set(name, entry);
450
+ changed = true;
451
+ }
452
+ if (changed) {
453
+ this._modelLoadersPreloaded = false;
454
+ this._modelLoadersPreloadPromise = void 0;
455
+ }
456
+ }
457
+ /**
458
+ * Get a registered model class (constructor) asynchronously.
459
+ * This will first ensure the model loader entry is resolved.
460
+ * @param {string} name Model class name
461
+ * @returns {Promise<ModelConstructor | undefined>} Model constructor, or undefined if not found
462
+ */
463
+ async getModelClassAsync(name) {
464
+ await this.ensureModel(name);
465
+ return this.getModelClass(name);
466
+ }
467
+ /**
468
+ * Get all registered model classes asynchronously.
469
+ * This will first ensure all registered model loader entries are resolved.
470
+ * @returns {Promise<Map<string, ModelConstructor>>} Model class map
471
+ */
472
+ async getModelClassesAsync() {
473
+ await this.ensureModels(Array.from(this._modelLoaders.keys()));
474
+ return this.getModelClasses();
475
+ }
476
+ /**
477
+ * Create and register a model instance asynchronously.
478
+ * This will first ensure all string-based model references in the model tree are resolved.
479
+ * @template T FlowModel subclass type, defaults to FlowModel.
480
+ * @param {CreateModelOptions} options Model creation options
481
+ * @returns {Promise<T>} Created model instance
482
+ */
483
+ async createModelAsync(options, extra) {
484
+ await this.resolveModelTree(options);
485
+ return this.createModel(options, extra);
486
+ }
487
+ /**
488
+ * Normalize a loader result into a model constructor.
489
+ * @param {string} name Model class name
490
+ * @param {FlowModelLoaderResult} loaded Loader result
491
+ * @returns {ModelConstructor | null} Normalized model constructor
492
+ * @private
493
+ */
494
+ normalizeModelLoaderResult(name, loaded) {
495
+ if (typeof loaded === "function") {
496
+ return loaded;
497
+ }
498
+ if (loaded && typeof loaded === "object") {
499
+ const defaultExport = loaded.default;
500
+ if (typeof defaultExport === "function") {
501
+ return defaultExport;
502
+ }
503
+ const namedExport = loaded[name];
504
+ if (typeof namedExport === "function") {
505
+ return namedExport;
506
+ }
507
+ }
508
+ console.warn(`FlowEngine: model loader for '${name}' did not resolve to a valid model constructor.`);
509
+ return null;
510
+ }
511
+ /**
512
+ * Collect string-based model names from a model tree.
513
+ * @param {unknown} data Model tree data
514
+ * @param {Set<string>} names Model name set
515
+ * @private
516
+ */
517
+ collectModelNamesFromTree(data, names) {
518
+ if (!data || typeof data !== "object") {
519
+ return;
520
+ }
521
+ if (Array.isArray(data)) {
522
+ data.forEach((item) => this.collectModelNamesFromTree(item, names));
523
+ return;
524
+ }
525
+ const tree = data;
526
+ if (typeof tree.use === "string") {
527
+ names.add(tree.use);
528
+ }
529
+ const subModels = tree.subModels;
530
+ if (!subModels || typeof subModels !== "object") {
531
+ return;
532
+ }
533
+ Object.values(subModels).forEach((value) => {
534
+ this.collectModelNamesFromTree(value, names);
535
+ });
536
+ }
537
+ /**
538
+ * Collect additional model names from object-form meta.createModelOptions defaults.
539
+ * @param {ModelConstructor} modelClass Model class constructor
540
+ * @param {Set<string>} names Model name set
541
+ * @private
542
+ */
543
+ collectModelNamesFromMetaDefaults(modelClass, names) {
544
+ var _a;
545
+ const metaCreate = (_a = modelClass.meta) == null ? void 0 : _a.createModelOptions;
546
+ if (metaCreate && typeof metaCreate === "object") {
547
+ this.collectModelNamesFromTree(metaCreate, names);
548
+ }
549
+ }
550
+ /**
551
+ * Ensure a single model class is available.
552
+ * @param {string} name Model class name
553
+ * @returns {Promise<ModelConstructor | null>} Model constructor or null when resolution fails
554
+ * @private
555
+ */
556
+ async ensureModel(name) {
557
+ const existing = this._modelClasses.get(name);
558
+ if (existing) {
559
+ return existing;
560
+ }
561
+ const inflight = this._loadingModelPromises.get(name);
562
+ if (inflight) {
563
+ return inflight;
564
+ }
565
+ const entry = this._modelLoaders.get(name);
566
+ if (!entry) {
567
+ console.warn(`FlowEngine: Model entry '${name}' not found. Falling back to ErrorFlowModel when needed.`);
568
+ return null;
569
+ }
570
+ const promise = (async () => {
571
+ try {
572
+ const loaded = await entry.loader();
573
+ const modelClass = this.normalizeModelLoaderResult(name, loaded);
574
+ if (!modelClass) {
575
+ return null;
576
+ }
577
+ this._registerModel(name, modelClass);
578
+ return modelClass;
579
+ } catch (error) {
580
+ console.warn(`FlowEngine: Failed to load model '${name}'. Falling back to ErrorFlowModel when needed.`, error);
581
+ return null;
582
+ } finally {
583
+ this._loadingModelPromises.delete(name);
584
+ }
585
+ })();
586
+ this._loadingModelPromises.set(name, promise);
587
+ return promise;
588
+ }
589
+ /**
590
+ * Ensure multiple model classes are available.
591
+ * @param {string[]} names Model class names
592
+ * @returns {Promise<EnsureBatchResult>} Batch ensure result
593
+ * @private
594
+ */
595
+ async ensureModels(names) {
596
+ const requested = Array.from(new Set(names.filter((name) => !!name)));
597
+ const loaded = [];
598
+ const failed = [];
599
+ const results = await Promise.all(
600
+ requested.map(async (name) => {
601
+ const modelClass = await this.ensureModel(name);
602
+ return { name, modelClass };
603
+ })
604
+ );
605
+ results.forEach(({ name, modelClass }) => {
606
+ if (modelClass) {
607
+ loaded.push(name);
608
+ } else {
609
+ failed.push({ name });
610
+ }
611
+ });
612
+ return { requested, loaded, failed };
613
+ }
614
+ /**
615
+ * Resolve all unresolved string-based model references in a model tree before synchronous creation begins.
616
+ *
617
+ * Use this when you already have a model tree object, such as repository-returned data or resolved
618
+ * `createModelOptions`, and you need to ensure every string `use` in that tree has been loaded and
619
+ * registered into `_modelClasses` before calling `createModel()`.
620
+ *
621
+ * @param {unknown} data Model tree data
622
+ * @returns {Promise<EnsureBatchResult>} Batch ensure result
623
+ */
624
+ async resolveModelTree(data) {
625
+ const requested = /* @__PURE__ */ new Set();
626
+ const loaded = /* @__PURE__ */ new Set();
627
+ const failed = /* @__PURE__ */ new Map();
628
+ const processed = /* @__PURE__ */ new Set();
629
+ const pending = /* @__PURE__ */ new Set();
630
+ this.collectModelNamesFromTree(data, pending);
631
+ while (pending.size > 0) {
632
+ const batch = Array.from(pending).filter((name) => !processed.has(name));
633
+ pending.clear();
634
+ if (batch.length === 0) {
635
+ break;
636
+ }
637
+ batch.forEach((name) => requested.add(name));
638
+ const result = await this.ensureModels(batch);
639
+ result.loaded.forEach((name) => {
640
+ processed.add(name);
641
+ loaded.add(name);
642
+ const modelClass = this.getModelClass(name);
643
+ if (modelClass) {
644
+ const discovered = /* @__PURE__ */ new Set();
645
+ this.collectModelNamesFromMetaDefaults(modelClass, discovered);
646
+ discovered.forEach((discoveredName) => {
647
+ if (!processed.has(discoveredName)) {
648
+ pending.add(discoveredName);
649
+ }
650
+ });
651
+ }
652
+ });
653
+ result.failed.forEach((item) => {
654
+ processed.add(item.name);
655
+ failed.set(item.name, item);
656
+ });
657
+ }
658
+ return {
659
+ requested: Array.from(requested),
660
+ loaded: Array.from(loaded),
661
+ failed: Array.from(failed.values())
662
+ };
663
+ }
664
+ /**
665
+ * Preload all currently registered unresolved model loaders.
666
+ *
667
+ * This method is intended for flow-settings/discovery style entry points that need registered model
668
+ * classes to exist before UI is rendered, without requiring callers to know which specific models
669
+ * will be touched next.
670
+ *
671
+ * @returns {Promise<EnsureBatchResult>} Batch ensure result
672
+ */
673
+ async preloadModelLoaders() {
674
+ const unresolved = Array.from(this._modelLoaders.keys()).filter((name) => !this._modelClasses.has(name));
675
+ if (unresolved.length === 0) {
676
+ this._modelLoadersPreloaded = true;
677
+ return { requested: [], loaded: [], failed: [] };
678
+ }
679
+ if (this._modelLoadersPreloadPromise) {
680
+ return this._modelLoadersPreloadPromise;
681
+ }
682
+ this._modelLoadersPreloadPromise = (async () => {
683
+ const result = await this.ensureModels(unresolved);
684
+ this._modelLoadersPreloaded = result.failed.length === 0;
685
+ this._modelLoadersPreloadPromise = void 0;
686
+ return result;
687
+ })();
688
+ return this._modelLoadersPreloadPromise;
689
+ }
387
690
  registerResources(resources) {
388
691
  for (const [name, resourceClass] of Object.entries(resources)) {
389
692
  this._resources.set(name, resourceClass);
@@ -447,6 +750,55 @@ const _FlowEngine = class _FlowEngine {
447
750
  }
448
751
  return result;
449
752
  }
753
+ /**
754
+ * Asynchronously get all subclasses of a base class, including those registered via model loaders.
755
+ * Merges results from already-loaded classes (_modelClasses) and async loader entries with matching `extends` declarations.
756
+ * Loader-resolved classes are validated with `isInheritedFrom`; mismatches are warned and excluded.
757
+ * @param {string | ModelConstructor} baseClass Base class name or constructor
758
+ * @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function
759
+ * @returns {Promise<Map<string, ModelConstructor>>} Model classes that are subclasses of the base class
760
+ */
761
+ async getSubclassesOfAsync(baseClass, filter) {
762
+ var _a;
763
+ const baseClassName = typeof baseClass === "string" ? baseClass : baseClass.name;
764
+ let parentModelClass;
765
+ if (typeof baseClass === "string") {
766
+ if (!this.getModelClass(baseClass)) {
767
+ await this.ensureModel(baseClass);
768
+ }
769
+ parentModelClass = this.getModelClass(baseClass);
770
+ } else {
771
+ parentModelClass = baseClass;
772
+ }
773
+ if (!parentModelClass) {
774
+ return /* @__PURE__ */ new Map();
775
+ }
776
+ const result = this.getSubclassesOf(parentModelClass, filter);
777
+ const loaderCandidates = [];
778
+ for (const [name, entry] of this._modelLoaders) {
779
+ if (result.has(name) || this._modelClasses.has(name)) continue;
780
+ if ((_a = entry.extends) == null ? void 0 : _a.includes(baseClassName)) {
781
+ loaderCandidates.push(name);
782
+ }
783
+ }
784
+ if (loaderCandidates.length > 0) {
785
+ await this.ensureModels(loaderCandidates);
786
+ }
787
+ for (const name of loaderCandidates) {
788
+ const ModelClass = this._modelClasses.get(name);
789
+ if (!ModelClass) continue;
790
+ if (!(0, import_utils.isInheritedFrom)(ModelClass, parentModelClass)) {
791
+ console.warn(
792
+ `FlowEngine: Model '${name}' declares extends '${baseClassName}' but does not actually inherit from it. Skipping.`
793
+ );
794
+ continue;
795
+ }
796
+ if (!filter || filter(ModelClass, name)) {
797
+ result.set(name, ModelClass);
798
+ }
799
+ }
800
+ return result;
801
+ }
450
802
  /**
451
803
  * Create and register a model instance.
452
804
  * If an instance with the same UID exists, returns the existing instance.
@@ -510,7 +862,6 @@ const _FlowEngine = class _FlowEngine {
510
862
  const visited = /* @__PURE__ */ new Set();
511
863
  while (current) {
512
864
  if (visited.has(current)) {
513
- console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
514
865
  break;
515
866
  }
516
867
  visited.add(current);
@@ -611,7 +962,7 @@ const _FlowEngine = class _FlowEngine {
611
962
  removeModel(uid) {
612
963
  var _a, _b, _c;
613
964
  if (!this._modelInstances.has(uid)) {
614
- console.warn(`FlowEngine: Model with UID '${uid}' does not exist.`);
965
+ this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
615
966
  return false;
616
967
  }
617
968
  const modelInstance = this._modelInstances.get(uid);
@@ -732,7 +1083,7 @@ const _FlowEngine = class _FlowEngine {
732
1083
  * Hydrate a model into current engine from an already-existing model instance in previous engines.
733
1084
  * - Avoids repository requests when the model tree is already present in memory.
734
1085
  */
735
- hydrateModelFromPreviousEngines(options, extra) {
1086
+ async hydrateModelFromPreviousEngines(options, extra) {
736
1087
  var _a;
737
1088
  const uid = options == null ? void 0 : options.uid;
738
1089
  const parentId = options == null ? void 0 : options.parentId;
@@ -744,7 +1095,7 @@ const _FlowEngine = class _FlowEngine {
744
1095
  }
745
1096
  if (existing) {
746
1097
  const data = existing.serialize();
747
- return this.createModel(data, extra);
1098
+ return this.createModelAsync(data, extra);
748
1099
  }
749
1100
  }
750
1101
  if (parentId && subKey) {
@@ -755,10 +1106,10 @@ const _FlowEngine = class _FlowEngine {
755
1106
  if (!localParent) {
756
1107
  const parentData = parentFromPrev.serialize();
757
1108
  delete parentData.subModels;
758
- localParent = this.createModel(parentData, extra);
1109
+ localParent = await this.createModelAsync(parentData, extra);
759
1110
  }
760
1111
  const modelData = modelFromPrev.serialize();
761
- const localModel = this.createModel(modelData, extra);
1112
+ const localModel = await this.createModelAsync(modelData, extra);
762
1113
  const mounted = (_a = localParent.subModels) == null ? void 0 : _a[subKey];
763
1114
  if (Array.isArray(mounted)) {
764
1115
  const exists = mounted.some((m) => (m == null ? void 0 : m.uid) === (localModel == null ? void 0 : localModel.uid));
@@ -789,25 +1140,36 @@ const _FlowEngine = class _FlowEngine {
789
1140
  async loadModel(options) {
790
1141
  if (!this.ensureModelRepository()) return;
791
1142
  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;
1143
+ const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
1144
+ if (!refresh && !bypassLoadedPageCache) {
1145
+ const model2 = this.findModelByParentId(options.parentId, options.subKey);
1146
+ if (model2) {
1147
+ return model2;
796
1148
  }
797
- const hydrated = this.hydrateModelFromPreviousEngines(options);
1149
+ const hydrated = await this.hydrateModelFromPreviousEngines(options);
798
1150
  if (hydrated) {
799
1151
  return hydrated;
800
1152
  }
801
1153
  }
802
1154
  const data = await this._modelRepository.findOne(options);
803
- if (!(data == null ? void 0 : data.uid)) return null;
804
- if (refresh) {
1155
+ if (!(data == null ? void 0 : data.uid)) {
1156
+ if (bypassLoadedPageCache) {
1157
+ this._loadedPageCache.clear(options);
1158
+ }
1159
+ return null;
1160
+ }
1161
+ if (refresh || bypassLoadedPageCache) {
805
1162
  const existing = this.getModel(data.uid);
806
1163
  if (existing) {
807
1164
  this.removeModelWithSubModels(existing.uid);
808
1165
  }
809
1166
  }
810
- return this.createModel(data);
1167
+ const model = await this.createModelAsync(data);
1168
+ if (bypassLoadedPageCache) {
1169
+ this._loadedPageCache.mountModelToParent(model, true);
1170
+ this._loadedPageCache.clear(options);
1171
+ }
1172
+ return model;
811
1173
  }
812
1174
  /**
813
1175
  * Find a sub-model by parent model ID and subKey.
@@ -838,39 +1200,39 @@ const _FlowEngine = class _FlowEngine {
838
1200
  async loadOrCreateModel(options, extra) {
839
1201
  if (!this.ensureModelRepository()) return;
840
1202
  const { uid, parentId, subKey } = options;
841
- if (uid && this._modelInstances.has(uid)) {
1203
+ const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
1204
+ if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
842
1205
  return this._modelInstances.get(uid);
843
1206
  }
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;
1207
+ if (!bypassLoadedPageCache) {
1208
+ const m = this.findModelByParentId(parentId, subKey);
1209
+ if (m) {
1210
+ return m;
1211
+ }
1212
+ const hydrated = await this.hydrateModelFromPreviousEngines(options, extra);
1213
+ if (hydrated) {
1214
+ return hydrated;
1215
+ }
851
1216
  }
852
1217
  const data = await this._modelRepository.findOne(options);
853
1218
  let model = null;
854
1219
  if (data == null ? void 0 : data.uid) {
855
- model = this.createModel(data, extra);
1220
+ if (bypassLoadedPageCache) {
1221
+ const existing = this.getModel(data.uid);
1222
+ if (existing) {
1223
+ this.removeModelWithSubModels(existing.uid);
1224
+ }
1225
+ }
1226
+ model = await this.createModelAsync(data, extra);
856
1227
  } else {
857
- model = this.createModel(options, extra);
1228
+ model = await this.createModelAsync(options, extra);
858
1229
  if (!(extra == null ? void 0 : extra.skipSave)) {
859
1230
  await model.save();
860
1231
  }
861
1232
  }
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
- }
1233
+ this._loadedPageCache.mountModelToParent(model, bypassLoadedPageCache);
1234
+ if (bypassLoadedPageCache) {
1235
+ this._loadedPageCache.clear(options);
874
1236
  }
875
1237
  return model;
876
1238
  }
@@ -888,6 +1250,9 @@ const _FlowEngine = class _FlowEngine {
888
1250
  async saveModel(model, options) {
889
1251
  if (!this.ensureModelRepository()) return;
890
1252
  const modelUid = model.uid;
1253
+ const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(model, {
1254
+ force: !!(options == null ? void 0 : options.onlyStepParams)
1255
+ });
891
1256
  if (this._savingModels.has(modelUid)) {
892
1257
  this.logger.debug(`Model ${modelUid} is already being saved, waiting for existing save operation`);
893
1258
  return await this._savingModels.get(modelUid);
@@ -896,6 +1261,7 @@ const _FlowEngine = class _FlowEngine {
896
1261
  this._savingModels.set(modelUid, savePromise);
897
1262
  try {
898
1263
  const result = await savePromise;
1264
+ this._loadedPageCache.markDirty(dirtyLoadedPageKey);
899
1265
  return result;
900
1266
  } finally {
901
1267
  this._savingModels.delete(modelUid);
@@ -926,10 +1292,15 @@ const _FlowEngine = class _FlowEngine {
926
1292
  * @returns {Promise<boolean>} Whether destroyed successfully
927
1293
  */
928
1294
  async destroyModel(uid) {
929
- if (this.ensureModelRepository()) {
1295
+ const modelInstance = this._modelInstances.get(uid);
1296
+ const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(modelInstance);
1297
+ const hasModelRepository = this.ensureModelRepository();
1298
+ if (hasModelRepository) {
930
1299
  await this._modelRepository.destroy(uid);
931
1300
  }
932
- const modelInstance = this._modelInstances.get(uid);
1301
+ if (hasModelRepository) {
1302
+ this._loadedPageCache.markDirty(dirtyLoadedPageKey);
1303
+ }
933
1304
  const parent = modelInstance == null ? void 0 : modelInstance.parent;
934
1305
  const result = this.removeModel(uid);
935
1306
  parent && parent.emitter.emit("onSubModelDestroyed", modelInstance);
@@ -1005,18 +1376,25 @@ const _FlowEngine = class _FlowEngine {
1005
1376
  }
1006
1377
  /**
1007
1378
  * Move a model instance within its parent model.
1008
- * @param {any} sourceId Source model UID
1009
- * @param {any} targetId Target model UID
1379
+ * @param {string | number} sourceId Source model UID
1380
+ * @param {string | number} targetId Target model UID
1010
1381
  * @returns {Promise<void>} No return value
1011
1382
  */
1012
1383
  async moveModel(sourceId, targetId, options) {
1013
1384
  var _a, _b;
1014
- const sourceModel = this.getModel(sourceId);
1015
- const targetModel = this.getModel(targetId);
1385
+ const sourceUid = String(sourceId);
1386
+ const targetUid = String(targetId);
1387
+ if (!sourceUid || !targetUid || sourceUid === targetUid) {
1388
+ return;
1389
+ }
1390
+ const sourceModel = this.getModel(sourceUid);
1391
+ const targetModel = this.getModel(targetUid);
1016
1392
  if (!sourceModel || !targetModel) {
1017
1393
  console.warn(`FlowEngine: Cannot move model. Source or target model not found.`);
1018
1394
  return;
1019
1395
  }
1396
+ let position = "after";
1397
+ const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(sourceModel);
1020
1398
  const move = /* @__PURE__ */ __name((sourceModel2, targetModel2) => {
1021
1399
  if (!sourceModel2.parent || !targetModel2.parent || sourceModel2.parent !== targetModel2.parent) {
1022
1400
  console.error("FlowModel.moveTo: Both models must have the same parent to perform move operation.");
@@ -1039,6 +1417,7 @@ const _FlowEngine = class _FlowEngine {
1039
1417
  console.warn("FlowModel.moveTo: Current model is already at the target position. No action taken.");
1040
1418
  return false;
1041
1419
  }
1420
+ position = currentIndex < targetIndex ? "after" : "before";
1042
1421
  const [movedModel] = subModelsCopy.splice(currentIndex, 1);
1043
1422
  subModelsCopy.splice(targetIndex, 0, movedModel);
1044
1423
  subModelsCopy.forEach((model, index) => {
@@ -1047,10 +1426,13 @@ const _FlowEngine = class _FlowEngine {
1047
1426
  subModels.splice(0, subModels.length, ...subModelsCopy);
1048
1427
  return true;
1049
1428
  }, "move");
1050
- move(sourceModel, targetModel);
1429
+ const moved = move(sourceModel, targetModel);
1430
+ if (!moved) {
1431
+ return;
1432
+ }
1051
1433
  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);
1434
+ await this._modelRepository.move(sourceUid, targetUid, position);
1435
+ this._loadedPageCache.markDirty(dirtyLoadedPageKey);
1054
1436
  }
1055
1437
  sourceModel.parent.emitter.emit("onSubModelMoved", { source: sourceModel, target: targetModel });
1056
1438
  (_b = this.emitter) == null ? void 0 : _b.emit("model:subModel:moved", {
@@ -1098,11 +1480,7 @@ _FlowEngine_instances = new WeakSet();
1098
1480
  * @private
1099
1481
  */
1100
1482
  registerModel_fn = /* @__PURE__ */ __name(function(name, modelClass) {
1101
- if (this._modelClasses.has(name)) {
1102
- console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
1103
- }
1104
- Object.defineProperty(modelClass, "name", { value: name });
1105
- this._modelClasses.set(name, modelClass);
1483
+ return this._registerModel(name, modelClass);
1106
1484
  }, "#registerModel");
1107
1485
  __name(_FlowEngine, "FlowEngine");
1108
1486
  let FlowEngine = _FlowEngine;
package/lib/flowI18n.js CHANGED
@@ -71,7 +71,8 @@ const _FlowI18n = class _FlowI18n {
71
71
  translateKey(key, options) {
72
72
  var _a, _b;
73
73
  if ((_b = (_a = this.context) == null ? void 0 : _a.i18n) == null ? void 0 : _b.t) {
74
- return this.context.i18n.t(key, options);
74
+ const translated = this.context.i18n.t(key, options);
75
+ return translated == null || translated === "" ? key : translated;
75
76
  }
76
77
  return key;
77
78
  }