@nocobase/flow-engine 2.1.0-beta.9 → 2.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +607 -19
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +12 -1
- package/lib/components/subModel/LazyDropdown.js +301 -52
- package/lib/components/subModel/index.d.ts +1 -0
- package/lib/components/subModel/index.js +19 -0
- package/lib/components/subModel/utils.d.ts +2 -1
- package/lib/components/subModel/utils.js +15 -5
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +84 -0
- package/lib/data-source/index.js +269 -7
- package/lib/executor/FlowExecutor.js +6 -3
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +9 -1
- package/lib/flowContext.js +77 -6
- package/lib/flowEngine.d.ts +136 -4
- package/lib/flowEngine.js +429 -51
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +13 -10
- package/lib/models/flowModel.js +126 -34
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
- package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/base.js +464 -29
- package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
- package/lib/runjs-context/contexts/elementDoc.js +152 -0
- package/lib/runjs-context/setup.js +1 -0
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/types.d.ts +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/loadedPageCache.d.ts +24 -0
- package/lib/utils/loadedPageCache.js +139 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +28 -4
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.d.ts +12 -2
- package/lib/views/ViewNavigation.js +28 -9
- package/lib/views/createViewMeta.js +114 -50
- package/lib/views/inheritLayoutContext.d.ts +10 -0
- package/lib/views/inheritLayoutContext.js +50 -0
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +12 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +12 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +5 -4
- package/src/FlowContextProvider.tsx +9 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +105 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
- package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +21 -0
- package/src/__tests__/runjsContextImplementations.test.ts +9 -2
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsLocales.test.ts +6 -5
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +750 -17
- package/src/components/dnd/index.tsx +305 -28
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +16 -2
- package/src/components/subModel/LazyDropdown.tsx +341 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +13 -2
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +69 -2
- package/src/data-source/index.ts +332 -8
- package/src/executor/FlowExecutor.ts +6 -3
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +85 -6
- package/src/flowEngine.ts +484 -45
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +65 -37
- package/src/models/flowModel.tsx +184 -65
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
- package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/base.ts +467 -31
- package/src/runjs-context/contexts/elementDoc.ts +130 -0
- package/src/runjs-context/setup.ts +1 -0
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +5 -1
- package/src/utils/loadedPageCache.ts +147 -0
- package/src/utils/parsePathnameToViewParams.ts +45 -5
- package/src/utils/randomId.ts +48 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +46 -9
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +13 -3
- package/src/views/useDrawer.tsx +13 -3
- package/src/views/usePage.tsx +367 -180
package/src/flowEngine.ts
CHANGED
|
@@ -20,11 +20,16 @@ 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,
|
|
26
27
|
CreateModelOptions,
|
|
28
|
+
EnsureBatchResult,
|
|
27
29
|
EventDefinition,
|
|
30
|
+
FlowModelLoaderEntry,
|
|
31
|
+
FlowModelLoaderInputMap,
|
|
32
|
+
FlowModelLoaderResult,
|
|
28
33
|
FlowModelOptions,
|
|
29
34
|
IFlowModelRepository,
|
|
30
35
|
ModelConstructor,
|
|
@@ -35,6 +40,8 @@ import type {
|
|
|
35
40
|
} from './types';
|
|
36
41
|
import { isInheritedFrom } from './utils';
|
|
37
42
|
|
|
43
|
+
const getFlowEngineLoggerLevel = () => (process.env.NODE_ENV === 'production' ? 'warn' : 'trace');
|
|
44
|
+
|
|
38
45
|
/**
|
|
39
46
|
* FlowEngine is the core class of the flow engine, responsible for managing flow models, actions, model repository, and more.
|
|
40
47
|
* It provides capabilities for registering, creating, finding, persisting, replacing, and moving models.
|
|
@@ -75,6 +82,32 @@ export class FlowEngine {
|
|
|
75
82
|
*/
|
|
76
83
|
private _modelClasses: Map<string, ModelConstructor> = observable.shallow(new Map());
|
|
77
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Registered model entries.
|
|
87
|
+
* Key is the model class name, value is the model loader entry.
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
private _modelLoaders: Map<string, FlowModelLoaderEntry> = new Map();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* In-flight model loading promises.
|
|
94
|
+
* Key is the model class name, value is the loading promise.
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
private _loadingModelPromises: Map<string, Promise<ModelConstructor | null>> = new Map();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether model-loader preload has completed in this session.
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
private _modelLoadersPreloaded = false;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* In-flight model-loader preload promise.
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
private _modelLoadersPreloadPromise?: Promise<EnsureBatchResult>;
|
|
110
|
+
|
|
78
111
|
/**
|
|
79
112
|
* Created model instances.
|
|
80
113
|
* Key is the model instance UID, value is the model instance object.
|
|
@@ -103,6 +136,8 @@ export class FlowEngine {
|
|
|
103
136
|
*/
|
|
104
137
|
private _savingModels = new Map<string, Promise<any>>();
|
|
105
138
|
|
|
139
|
+
private _loadedPageCache = createLoadedPageCache();
|
|
140
|
+
|
|
106
141
|
/**
|
|
107
142
|
* Flow engine context object.
|
|
108
143
|
* @private
|
|
@@ -183,7 +218,7 @@ export class FlowEngine {
|
|
|
183
218
|
MultiRecordResource,
|
|
184
219
|
});
|
|
185
220
|
this.logger = pino({
|
|
186
|
-
level:
|
|
221
|
+
level: getFlowEngineLoggerLevel(),
|
|
187
222
|
browser: {
|
|
188
223
|
write: {
|
|
189
224
|
fatal: (o) => console.trace(o),
|
|
@@ -424,6 +459,13 @@ export class FlowEngine {
|
|
|
424
459
|
* @private
|
|
425
460
|
*/
|
|
426
461
|
#registerModel(name: string, modelClass: ModelConstructor): void {
|
|
462
|
+
return this._registerModel(name, modelClass);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* for proxy instance, the #registerModel can't be called.
|
|
467
|
+
*/
|
|
468
|
+
private _registerModel(name: string, modelClass: ModelConstructor): void {
|
|
427
469
|
if (this._modelClasses.has(name)) {
|
|
428
470
|
console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
|
|
429
471
|
}
|
|
@@ -444,6 +486,306 @@ export class FlowEngine {
|
|
|
444
486
|
}
|
|
445
487
|
}
|
|
446
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Register multiple model loader entries.
|
|
491
|
+
* The `extends` field declares parent class(es) for async subclass discovery via `getSubclassesOfAsync`.
|
|
492
|
+
* It accepts `string | ModelConstructor | (string | ModelConstructor)[]` and is normalized to `string[]` internally.
|
|
493
|
+
* @param {FlowModelLoaderInputMap} loaders Model loader input map
|
|
494
|
+
* @returns {void}
|
|
495
|
+
* @example
|
|
496
|
+
* flowEngine.registerModelLoaders({
|
|
497
|
+
* DemoModel: {
|
|
498
|
+
* extends: 'BaseModel',
|
|
499
|
+
* loader: () => import('./models/DemoModel'),
|
|
500
|
+
* },
|
|
501
|
+
* });
|
|
502
|
+
*/
|
|
503
|
+
public registerModelLoaders(loaders: FlowModelLoaderInputMap): void {
|
|
504
|
+
let changed = false;
|
|
505
|
+
for (const [name, input] of Object.entries(loaders)) {
|
|
506
|
+
if (this._modelLoaders.has(name)) {
|
|
507
|
+
console.warn(`FlowEngine: Model loader with name '${name}' is already registered and will be overwritten.`);
|
|
508
|
+
}
|
|
509
|
+
const entry: FlowModelLoaderEntry = {
|
|
510
|
+
loader: input.loader,
|
|
511
|
+
};
|
|
512
|
+
if (input.extends != null) {
|
|
513
|
+
const raw = Array.isArray(input.extends) ? input.extends : [input.extends];
|
|
514
|
+
entry.extends = raw.map((item) => (typeof item === 'string' ? item : item.name));
|
|
515
|
+
}
|
|
516
|
+
this._modelLoaders.set(name, entry);
|
|
517
|
+
changed = true;
|
|
518
|
+
}
|
|
519
|
+
if (changed) {
|
|
520
|
+
this._modelLoadersPreloaded = false;
|
|
521
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Get a registered model class (constructor) asynchronously.
|
|
527
|
+
* This will first ensure the model loader entry is resolved.
|
|
528
|
+
* @param {string} name Model class name
|
|
529
|
+
* @returns {Promise<ModelConstructor | undefined>} Model constructor, or undefined if not found
|
|
530
|
+
*/
|
|
531
|
+
public async getModelClassAsync(name: string): Promise<ModelConstructor | undefined> {
|
|
532
|
+
await this.ensureModel(name);
|
|
533
|
+
return this.getModelClass(name);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get all registered model classes asynchronously.
|
|
538
|
+
* This will first ensure all registered model loader entries are resolved.
|
|
539
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model class map
|
|
540
|
+
*/
|
|
541
|
+
public async getModelClassesAsync(): Promise<Map<string, ModelConstructor>> {
|
|
542
|
+
await this.ensureModels(Array.from(this._modelLoaders.keys()));
|
|
543
|
+
return this.getModelClasses();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Create and register a model instance asynchronously.
|
|
548
|
+
* This will first ensure all string-based model references in the model tree are resolved.
|
|
549
|
+
* @template T FlowModel subclass type, defaults to FlowModel.
|
|
550
|
+
* @param {CreateModelOptions} options Model creation options
|
|
551
|
+
* @returns {Promise<T>} Created model instance
|
|
552
|
+
*/
|
|
553
|
+
public async createModelAsync<T extends FlowModel = FlowModel>(
|
|
554
|
+
options: CreateModelOptions,
|
|
555
|
+
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
556
|
+
): Promise<T> {
|
|
557
|
+
await this.resolveModelTree(options);
|
|
558
|
+
return this.createModel<T>(options, extra);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Normalize a loader result into a model constructor.
|
|
563
|
+
* @param {string} name Model class name
|
|
564
|
+
* @param {FlowModelLoaderResult} loaded Loader result
|
|
565
|
+
* @returns {ModelConstructor | null} Normalized model constructor
|
|
566
|
+
* @private
|
|
567
|
+
*/
|
|
568
|
+
private normalizeModelLoaderResult(name: string, loaded: FlowModelLoaderResult): ModelConstructor | null {
|
|
569
|
+
if (typeof loaded === 'function') {
|
|
570
|
+
return loaded as ModelConstructor;
|
|
571
|
+
}
|
|
572
|
+
if (loaded && typeof loaded === 'object') {
|
|
573
|
+
const defaultExport = loaded.default;
|
|
574
|
+
if (typeof defaultExport === 'function') {
|
|
575
|
+
return defaultExport as ModelConstructor;
|
|
576
|
+
}
|
|
577
|
+
const namedExport = loaded[name];
|
|
578
|
+
if (typeof namedExport === 'function') {
|
|
579
|
+
return namedExport as ModelConstructor;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
console.warn(`FlowEngine: model loader for '${name}' did not resolve to a valid model constructor.`);
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Collect string-based model names from a model tree.
|
|
588
|
+
* @param {unknown} data Model tree data
|
|
589
|
+
* @param {Set<string>} names Model name set
|
|
590
|
+
* @private
|
|
591
|
+
*/
|
|
592
|
+
private collectModelNamesFromTree(data: unknown, names: Set<string>): void {
|
|
593
|
+
if (!data || typeof data !== 'object') {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (Array.isArray(data)) {
|
|
597
|
+
data.forEach((item) => this.collectModelNamesFromTree(item, names));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const tree = data as Record<string, any>;
|
|
602
|
+
if (typeof tree.use === 'string') {
|
|
603
|
+
names.add(tree.use);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const subModels = tree.subModels;
|
|
607
|
+
if (!subModels || typeof subModels !== 'object') {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
Object.values(subModels).forEach((value) => {
|
|
612
|
+
this.collectModelNamesFromTree(value, names);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Collect additional model names from object-form meta.createModelOptions defaults.
|
|
618
|
+
* @param {ModelConstructor} modelClass Model class constructor
|
|
619
|
+
* @param {Set<string>} names Model name set
|
|
620
|
+
* @private
|
|
621
|
+
*/
|
|
622
|
+
private collectModelNamesFromMetaDefaults(modelClass: ModelConstructor, names: Set<string>): void {
|
|
623
|
+
const metaCreate = (modelClass as typeof FlowModel).meta?.createModelOptions;
|
|
624
|
+
if (metaCreate && typeof metaCreate === 'object') {
|
|
625
|
+
this.collectModelNamesFromTree(metaCreate, names);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Ensure a single model class is available.
|
|
631
|
+
* @param {string} name Model class name
|
|
632
|
+
* @returns {Promise<ModelConstructor | null>} Model constructor or null when resolution fails
|
|
633
|
+
* @private
|
|
634
|
+
*/
|
|
635
|
+
private async ensureModel(name: string): Promise<ModelConstructor | null> {
|
|
636
|
+
const existing = this._modelClasses.get(name);
|
|
637
|
+
if (existing) {
|
|
638
|
+
return existing;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const inflight = this._loadingModelPromises.get(name);
|
|
642
|
+
if (inflight) {
|
|
643
|
+
return inflight;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const entry = this._modelLoaders.get(name);
|
|
647
|
+
if (!entry) {
|
|
648
|
+
console.warn(`FlowEngine: Model entry '${name}' not found. Falling back to ErrorFlowModel when needed.`);
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const promise = (async () => {
|
|
653
|
+
try {
|
|
654
|
+
const loaded = await entry.loader();
|
|
655
|
+
const modelClass = this.normalizeModelLoaderResult(name, loaded);
|
|
656
|
+
if (!modelClass) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
// 这里拿到的 this 是 Proxy(FlowEngine) 而不是原始的 FlowEngine,无法直接调用 #registerModel
|
|
660
|
+
this._registerModel(name, modelClass);
|
|
661
|
+
return modelClass;
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.warn(`FlowEngine: Failed to load model '${name}'. Falling back to ErrorFlowModel when needed.`, error);
|
|
664
|
+
return null;
|
|
665
|
+
} finally {
|
|
666
|
+
this._loadingModelPromises.delete(name);
|
|
667
|
+
}
|
|
668
|
+
})();
|
|
669
|
+
|
|
670
|
+
this._loadingModelPromises.set(name, promise);
|
|
671
|
+
return promise;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Ensure multiple model classes are available.
|
|
676
|
+
* @param {string[]} names Model class names
|
|
677
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
678
|
+
* @private
|
|
679
|
+
*/
|
|
680
|
+
private async ensureModels(names: string[]): Promise<EnsureBatchResult> {
|
|
681
|
+
const requested = Array.from(new Set(names.filter((name): name is string => !!name)));
|
|
682
|
+
const loaded: string[] = [];
|
|
683
|
+
const failed: EnsureBatchResult['failed'] = [];
|
|
684
|
+
|
|
685
|
+
const results = await Promise.all(
|
|
686
|
+
requested.map(async (name) => {
|
|
687
|
+
const modelClass = await this.ensureModel(name);
|
|
688
|
+
return { name, modelClass };
|
|
689
|
+
}),
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
results.forEach(({ name, modelClass }) => {
|
|
693
|
+
if (modelClass) {
|
|
694
|
+
loaded.push(name);
|
|
695
|
+
} else {
|
|
696
|
+
failed.push({ name });
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
return { requested, loaded, failed };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Resolve all unresolved string-based model references in a model tree before synchronous creation begins.
|
|
705
|
+
*
|
|
706
|
+
* Use this when you already have a model tree object, such as repository-returned data or resolved
|
|
707
|
+
* `createModelOptions`, and you need to ensure every string `use` in that tree has been loaded and
|
|
708
|
+
* registered into `_modelClasses` before calling `createModel()`.
|
|
709
|
+
*
|
|
710
|
+
* @param {unknown} data Model tree data
|
|
711
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
712
|
+
*/
|
|
713
|
+
public async resolveModelTree(data: unknown): Promise<EnsureBatchResult> {
|
|
714
|
+
const requested = new Set<string>();
|
|
715
|
+
const loaded = new Set<string>();
|
|
716
|
+
const failed = new Map<string, { name: string; error?: unknown }>();
|
|
717
|
+
const processed = new Set<string>();
|
|
718
|
+
const pending = new Set<string>();
|
|
719
|
+
|
|
720
|
+
this.collectModelNamesFromTree(data, pending);
|
|
721
|
+
|
|
722
|
+
while (pending.size > 0) {
|
|
723
|
+
const batch = Array.from(pending).filter((name) => !processed.has(name));
|
|
724
|
+
pending.clear();
|
|
725
|
+
if (batch.length === 0) {
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
batch.forEach((name) => requested.add(name));
|
|
730
|
+
const result = await this.ensureModels(batch);
|
|
731
|
+
|
|
732
|
+
result.loaded.forEach((name) => {
|
|
733
|
+
processed.add(name);
|
|
734
|
+
loaded.add(name);
|
|
735
|
+
const modelClass = this.getModelClass(name);
|
|
736
|
+
if (modelClass) {
|
|
737
|
+
const discovered = new Set<string>();
|
|
738
|
+
this.collectModelNamesFromMetaDefaults(modelClass, discovered);
|
|
739
|
+
discovered.forEach((discoveredName) => {
|
|
740
|
+
if (!processed.has(discoveredName)) {
|
|
741
|
+
pending.add(discoveredName);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
result.failed.forEach((item) => {
|
|
748
|
+
processed.add(item.name);
|
|
749
|
+
failed.set(item.name, item);
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
requested: Array.from(requested),
|
|
755
|
+
loaded: Array.from(loaded),
|
|
756
|
+
failed: Array.from(failed.values()),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Preload all currently registered unresolved model loaders.
|
|
762
|
+
*
|
|
763
|
+
* This method is intended for flow-settings/discovery style entry points that need registered model
|
|
764
|
+
* classes to exist before UI is rendered, without requiring callers to know which specific models
|
|
765
|
+
* will be touched next.
|
|
766
|
+
*
|
|
767
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
768
|
+
*/
|
|
769
|
+
public async preloadModelLoaders(): Promise<EnsureBatchResult> {
|
|
770
|
+
const unresolved = Array.from(this._modelLoaders.keys()).filter((name) => !this._modelClasses.has(name));
|
|
771
|
+
if (unresolved.length === 0) {
|
|
772
|
+
this._modelLoadersPreloaded = true;
|
|
773
|
+
return { requested: [], loaded: [], failed: [] };
|
|
774
|
+
}
|
|
775
|
+
if (this._modelLoadersPreloadPromise) {
|
|
776
|
+
return this._modelLoadersPreloadPromise;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
this._modelLoadersPreloadPromise = (async () => {
|
|
780
|
+
const result = await this.ensureModels(unresolved);
|
|
781
|
+
this._modelLoadersPreloaded = result.failed.length === 0;
|
|
782
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
783
|
+
return result;
|
|
784
|
+
})();
|
|
785
|
+
|
|
786
|
+
return this._modelLoadersPreloadPromise;
|
|
787
|
+
}
|
|
788
|
+
|
|
447
789
|
registerResources(resources: Record<string, any>) {
|
|
448
790
|
for (const [name, resourceClass] of Object.entries(resources)) {
|
|
449
791
|
this._resources.set(name, resourceClass);
|
|
@@ -518,6 +860,70 @@ export class FlowEngine {
|
|
|
518
860
|
return result;
|
|
519
861
|
}
|
|
520
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Asynchronously get all subclasses of a base class, including those registered via model loaders.
|
|
865
|
+
* Merges results from already-loaded classes (_modelClasses) and async loader entries with matching `extends` declarations.
|
|
866
|
+
* Loader-resolved classes are validated with `isInheritedFrom`; mismatches are warned and excluded.
|
|
867
|
+
* @param {string | ModelConstructor} baseClass Base class name or constructor
|
|
868
|
+
* @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function
|
|
869
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model classes that are subclasses of the base class
|
|
870
|
+
*/
|
|
871
|
+
public async getSubclassesOfAsync(
|
|
872
|
+
baseClass: string | ModelConstructor,
|
|
873
|
+
filter?: (ModelClass: ModelConstructor, className: string) => boolean,
|
|
874
|
+
): Promise<Map<string, ModelConstructor>> {
|
|
875
|
+
const baseClassName = typeof baseClass === 'string' ? baseClass : baseClass.name;
|
|
876
|
+
|
|
877
|
+
// If baseClass is a string and not yet loaded, try to resolve it first
|
|
878
|
+
let parentModelClass: ModelConstructor | undefined;
|
|
879
|
+
if (typeof baseClass === 'string') {
|
|
880
|
+
if (!this.getModelClass(baseClass)) {
|
|
881
|
+
await this.ensureModel(baseClass);
|
|
882
|
+
}
|
|
883
|
+
parentModelClass = this.getModelClass(baseClass);
|
|
884
|
+
} else {
|
|
885
|
+
parentModelClass = baseClass;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (!parentModelClass) {
|
|
889
|
+
return new Map();
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Step 1: Collect already-loaded subclasses from _modelClasses
|
|
893
|
+
const result = this.getSubclassesOf(parentModelClass, filter);
|
|
894
|
+
|
|
895
|
+
// Step 2: Find unloaded loaders whose extends includes baseClassName
|
|
896
|
+
const loaderCandidates: string[] = [];
|
|
897
|
+
for (const [name, entry] of this._modelLoaders) {
|
|
898
|
+
if (result.has(name) || this._modelClasses.has(name)) continue;
|
|
899
|
+
if (entry.extends?.includes(baseClassName)) {
|
|
900
|
+
loaderCandidates.push(name);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Step 3: Resolve all matching loaders
|
|
905
|
+
if (loaderCandidates.length > 0) {
|
|
906
|
+
await this.ensureModels(loaderCandidates);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Step 4: Validate resolved classes and add to result
|
|
910
|
+
for (const name of loaderCandidates) {
|
|
911
|
+
const ModelClass = this._modelClasses.get(name);
|
|
912
|
+
if (!ModelClass) continue;
|
|
913
|
+
if (!isInheritedFrom(ModelClass, parentModelClass)) {
|
|
914
|
+
console.warn(
|
|
915
|
+
`FlowEngine: Model '${name}' declares extends '${baseClassName}' but does not actually inherit from it. Skipping.`,
|
|
916
|
+
);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (!filter || filter(ModelClass, name)) {
|
|
920
|
+
result.set(name, ModelClass);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return result;
|
|
925
|
+
}
|
|
926
|
+
|
|
521
927
|
/**
|
|
522
928
|
* Create and register a model instance.
|
|
523
929
|
* If an instance with the same UID exists, returns the existing instance.
|
|
@@ -608,7 +1014,6 @@ export class FlowEngine {
|
|
|
608
1014
|
|
|
609
1015
|
while (current) {
|
|
610
1016
|
if (visited.has(current)) {
|
|
611
|
-
console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
|
|
612
1017
|
break;
|
|
613
1018
|
}
|
|
614
1019
|
visited.add(current);
|
|
@@ -727,7 +1132,7 @@ export class FlowEngine {
|
|
|
727
1132
|
*/
|
|
728
1133
|
public removeModel(uid: string): boolean {
|
|
729
1134
|
if (!this._modelInstances.has(uid)) {
|
|
730
|
-
|
|
1135
|
+
this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
|
|
731
1136
|
return false;
|
|
732
1137
|
}
|
|
733
1138
|
const modelInstance = this._modelInstances.get(uid) as FlowModel;
|
|
@@ -865,10 +1270,10 @@ export class FlowEngine {
|
|
|
865
1270
|
* Hydrate a model into current engine from an already-existing model instance in previous engines.
|
|
866
1271
|
* - Avoids repository requests when the model tree is already present in memory.
|
|
867
1272
|
*/
|
|
868
|
-
private hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
1273
|
+
private async hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
869
1274
|
options: any,
|
|
870
1275
|
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
871
|
-
): T | null {
|
|
1276
|
+
): Promise<T | null> {
|
|
872
1277
|
const uid = options?.uid;
|
|
873
1278
|
const parentId = options?.parentId;
|
|
874
1279
|
const subKey = options?.subKey;
|
|
@@ -882,7 +1287,7 @@ export class FlowEngine {
|
|
|
882
1287
|
}
|
|
883
1288
|
if (existing) {
|
|
884
1289
|
const data = existing.serialize();
|
|
885
|
-
return this.
|
|
1290
|
+
return this.createModelAsync<T>(data as any, extra);
|
|
886
1291
|
}
|
|
887
1292
|
}
|
|
888
1293
|
|
|
@@ -897,11 +1302,11 @@ export class FlowEngine {
|
|
|
897
1302
|
if (!localParent) {
|
|
898
1303
|
const parentData = parentFromPrev.serialize();
|
|
899
1304
|
delete (parentData as any).subModels;
|
|
900
|
-
localParent = this.
|
|
1305
|
+
localParent = await this.createModelAsync<FlowModel>(parentData as any, extra);
|
|
901
1306
|
}
|
|
902
1307
|
// Create (or reuse) the sub-model instance in current engine.
|
|
903
1308
|
const modelData = modelFromPrev.serialize();
|
|
904
|
-
const localModel = this.
|
|
1309
|
+
const localModel = await this.createModelAsync<T>(modelData as any, extra);
|
|
905
1310
|
|
|
906
1311
|
// Mount under local parent if not mounted yet (so later lookups by parentId/subKey won't hit repo).
|
|
907
1312
|
const mounted = (localParent.subModels as any)?.[subKey];
|
|
@@ -937,25 +1342,36 @@ export class FlowEngine {
|
|
|
937
1342
|
async loadModel<T extends FlowModel = FlowModel>(options): Promise<T | null> {
|
|
938
1343
|
if (!this.ensureModelRepository()) return;
|
|
939
1344
|
const refresh = !!options?.refresh;
|
|
940
|
-
|
|
1345
|
+
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1346
|
+
if (!refresh && !bypassLoadedPageCache) {
|
|
941
1347
|
const model = this.findModelByParentId(options.parentId, options.subKey);
|
|
942
1348
|
if (model) {
|
|
943
1349
|
return model as T;
|
|
944
1350
|
}
|
|
945
|
-
const hydrated = this.hydrateModelFromPreviousEngines<T>(options);
|
|
1351
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options);
|
|
946
1352
|
if (hydrated) {
|
|
947
1353
|
return hydrated as T;
|
|
948
1354
|
}
|
|
949
1355
|
}
|
|
950
1356
|
const data = await this._modelRepository.findOne(options);
|
|
951
|
-
if (!data?.uid)
|
|
952
|
-
|
|
1357
|
+
if (!data?.uid) {
|
|
1358
|
+
if (bypassLoadedPageCache) {
|
|
1359
|
+
this._loadedPageCache.clear(options);
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
if (refresh || bypassLoadedPageCache) {
|
|
953
1364
|
const existing = this.getModel(data.uid);
|
|
954
1365
|
if (existing) {
|
|
955
1366
|
this.removeModelWithSubModels(existing.uid);
|
|
956
1367
|
}
|
|
957
1368
|
}
|
|
958
|
-
|
|
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;
|
|
959
1375
|
}
|
|
960
1376
|
|
|
961
1377
|
/**
|
|
@@ -995,41 +1411,41 @@ export class FlowEngine {
|
|
|
995
1411
|
): Promise<T | null> {
|
|
996
1412
|
if (!this.ensureModelRepository()) return;
|
|
997
1413
|
const { uid, parentId, subKey } = options;
|
|
998
|
-
|
|
1414
|
+
const bypassLoadedPageCache = this._loadedPageCache.shouldBypass(options, () => this.context.flowSettingsEnabled);
|
|
1415
|
+
if (uid && !bypassLoadedPageCache && this._modelInstances.has(uid)) {
|
|
999
1416
|
return this._modelInstances.get(uid) as T;
|
|
1000
1417
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1418
|
+
if (!bypassLoadedPageCache) {
|
|
1419
|
+
const m = this.findModelByParentId<T>(parentId, subKey);
|
|
1420
|
+
if (m) {
|
|
1421
|
+
return m;
|
|
1422
|
+
}
|
|
1005
1423
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1424
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1425
|
+
if (hydrated) {
|
|
1426
|
+
return hydrated;
|
|
1427
|
+
}
|
|
1009
1428
|
}
|
|
1010
1429
|
|
|
1011
1430
|
const data = await this._modelRepository.findOne(options);
|
|
1012
1431
|
let model: T | null = null;
|
|
1013
1432
|
if (data?.uid) {
|
|
1014
|
-
|
|
1433
|
+
if (bypassLoadedPageCache) {
|
|
1434
|
+
const existing = this.getModel(data.uid);
|
|
1435
|
+
if (existing) {
|
|
1436
|
+
this.removeModelWithSubModels(existing.uid);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
model = await this.createModelAsync<T>(data as any, extra);
|
|
1015
1440
|
} else {
|
|
1016
|
-
model = this.
|
|
1441
|
+
model = await this.createModelAsync<T>(options, extra);
|
|
1017
1442
|
if (!extra?.skipSave) {
|
|
1018
1443
|
await model.save();
|
|
1019
1444
|
}
|
|
1020
1445
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
});
|
|
1025
|
-
if (subModel) {
|
|
1026
|
-
return model;
|
|
1027
|
-
}
|
|
1028
|
-
if (model.subType === 'array') {
|
|
1029
|
-
model.parent.addSubModel(model.subKey, model);
|
|
1030
|
-
} else {
|
|
1031
|
-
model.parent.setSubModel(model.subKey, model);
|
|
1032
|
-
}
|
|
1446
|
+
this._loadedPageCache.mountModelToParent(model, bypassLoadedPageCache);
|
|
1447
|
+
if (bypassLoadedPageCache) {
|
|
1448
|
+
this._loadedPageCache.clear(options);
|
|
1033
1449
|
}
|
|
1034
1450
|
return model;
|
|
1035
1451
|
}
|
|
@@ -1049,6 +1465,9 @@ export class FlowEngine {
|
|
|
1049
1465
|
if (!this.ensureModelRepository()) return;
|
|
1050
1466
|
|
|
1051
1467
|
const modelUid = model.uid;
|
|
1468
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(model, {
|
|
1469
|
+
force: !!options?.onlyStepParams,
|
|
1470
|
+
});
|
|
1052
1471
|
|
|
1053
1472
|
// 如果这个 model 正在保存中,返回现有的保存 Promise
|
|
1054
1473
|
if (this._savingModels.has(modelUid)) {
|
|
@@ -1062,6 +1481,7 @@ export class FlowEngine {
|
|
|
1062
1481
|
|
|
1063
1482
|
try {
|
|
1064
1483
|
const result = await savePromise;
|
|
1484
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1065
1485
|
return result;
|
|
1066
1486
|
} finally {
|
|
1067
1487
|
// 无论成功还是失败,都要清除保存状态
|
|
@@ -1098,11 +1518,16 @@ export class FlowEngine {
|
|
|
1098
1518
|
* @returns {Promise<boolean>} Whether destroyed successfully
|
|
1099
1519
|
*/
|
|
1100
1520
|
async destroyModel(uid: string) {
|
|
1101
|
-
|
|
1521
|
+
const modelInstance = this._modelInstances.get(uid) as FlowModel;
|
|
1522
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(modelInstance);
|
|
1523
|
+
const hasModelRepository = this.ensureModelRepository();
|
|
1524
|
+
if (hasModelRepository) {
|
|
1102
1525
|
await this._modelRepository.destroy(uid);
|
|
1103
1526
|
}
|
|
1104
1527
|
|
|
1105
|
-
|
|
1528
|
+
if (hasModelRepository) {
|
|
1529
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1530
|
+
}
|
|
1106
1531
|
const parent = modelInstance?.parent;
|
|
1107
1532
|
const result = this.removeModel(uid);
|
|
1108
1533
|
parent && parent.emitter.emit('onSubModelDestroyed', modelInstance);
|
|
@@ -1206,17 +1631,25 @@ export class FlowEngine {
|
|
|
1206
1631
|
|
|
1207
1632
|
/**
|
|
1208
1633
|
* Move a model instance within its parent model.
|
|
1209
|
-
* @param {
|
|
1210
|
-
* @param {
|
|
1634
|
+
* @param {string | number} sourceId Source model UID
|
|
1635
|
+
* @param {string | number} targetId Target model UID
|
|
1211
1636
|
* @returns {Promise<void>} No return value
|
|
1212
1637
|
*/
|
|
1213
|
-
async moveModel(sourceId:
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
1638
|
+
async moveModel(sourceId: string | number, targetId: string | number, options?: PersistOptions): Promise<void> {
|
|
1639
|
+
const sourceUid = String(sourceId);
|
|
1640
|
+
const targetUid = String(targetId);
|
|
1641
|
+
if (!sourceUid || !targetUid || sourceUid === targetUid) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const sourceModel = this.getModel(sourceUid);
|
|
1646
|
+
const targetModel = this.getModel(targetUid);
|
|
1216
1647
|
if (!sourceModel || !targetModel) {
|
|
1217
1648
|
console.warn(`FlowEngine: Cannot move model. Source or target model not found.`);
|
|
1218
1649
|
return;
|
|
1219
1650
|
}
|
|
1651
|
+
let position: 'before' | 'after' = 'after';
|
|
1652
|
+
const dirtyLoadedPageKey = this._loadedPageCache.getDirtyKeyForModel(sourceModel);
|
|
1220
1653
|
const move = (sourceModel: FlowModel, targetModel: FlowModel) => {
|
|
1221
1654
|
if (!sourceModel.parent || !targetModel.parent || sourceModel.parent !== targetModel.parent) {
|
|
1222
1655
|
console.error('FlowModel.moveTo: Both models must have the same parent to perform move operation.');
|
|
@@ -1246,6 +1679,8 @@ export class FlowEngine {
|
|
|
1246
1679
|
return false;
|
|
1247
1680
|
}
|
|
1248
1681
|
|
|
1682
|
+
position = currentIndex < targetIndex ? 'after' : 'before';
|
|
1683
|
+
|
|
1249
1684
|
// 使用splice直接移动数组元素(O(n)比排序O(n log n)更快)
|
|
1250
1685
|
const [movedModel] = subModelsCopy.splice(currentIndex, 1);
|
|
1251
1686
|
subModelsCopy.splice(targetIndex, 0, movedModel);
|
|
@@ -1260,10 +1695,14 @@ export class FlowEngine {
|
|
|
1260
1695
|
|
|
1261
1696
|
return true;
|
|
1262
1697
|
};
|
|
1263
|
-
move(sourceModel, targetModel);
|
|
1698
|
+
const moved = move(sourceModel, targetModel);
|
|
1699
|
+
if (!moved) {
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1264
1703
|
if (options?.persist !== false && this.ensureModelRepository()) {
|
|
1265
|
-
|
|
1266
|
-
|
|
1704
|
+
await this._modelRepository.move(sourceUid, targetUid, position);
|
|
1705
|
+
this._loadedPageCache.markDirty(dirtyLoadedPageKey);
|
|
1267
1706
|
}
|
|
1268
1707
|
// 触发事件以通知其他部分模型已移动
|
|
1269
1708
|
sourceModel.parent.emitter.emit('onSubModelMoved', { source: sourceModel, target: targetModel });
|