@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45
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/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- package/lib/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- 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 +613 -21
- 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 +76 -11
- 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/StepSettingsDialog.js +16 -2
- 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 +27 -1
- package/lib/components/subModel/LazyDropdown.js +293 -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 +1 -1
- package/lib/components/subModel/utils.js +9 -3
- 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 +259 -5
- package/lib/executor/FlowExecutor.js +32 -9
- 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 +3 -0
- package/lib/flowContext.js +46 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +392 -18
- 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 +81 -21
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +20 -12
- 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/scheduler/ModelOperationScheduler.d.ts +5 -1
- package/lib/scheduler/ModelOperationScheduler.js +3 -2
- 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/parsePathnameToViewParams.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +29 -5
- 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 +22 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +22 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +6 -5
- package/src/FlowContextProvider.tsx +9 -1
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -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 +82 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- 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 +16 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -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 +558 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +758 -19
- 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 +99 -11
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +194 -5
- 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 +32 -2
- package/src/components/subModel/LazyDropdown.tsx +332 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +7 -1
- 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 +68 -1
- package/src/data-source/index.ts +322 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +85 -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 +50 -3
- package/src/flowEngine.ts +449 -14
- 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__/dispatchEvent.when.test.ts +214 -0
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +80 -37
- package/src/models/flowModel.tsx +122 -36
- 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/registry.ts +1 -1
- package/src/runjs-context/setup.ts +22 -12
- 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/scheduler/ModelOperationScheduler.ts +14 -3
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +28 -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/parsePathnameToViewParams.ts +47 -7
- 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 +13 -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 +27 -3
- package/src/views/useDrawer.tsx +27 -3
- package/src/views/usePage.tsx +367 -179
package/src/flowEngine.ts
CHANGED
|
@@ -24,7 +24,11 @@ import type {
|
|
|
24
24
|
ActionDefinition,
|
|
25
25
|
ApplyFlowCacheEntry,
|
|
26
26
|
CreateModelOptions,
|
|
27
|
+
EnsureBatchResult,
|
|
27
28
|
EventDefinition,
|
|
29
|
+
FlowModelLoaderEntry,
|
|
30
|
+
FlowModelLoaderInputMap,
|
|
31
|
+
FlowModelLoaderResult,
|
|
28
32
|
FlowModelOptions,
|
|
29
33
|
IFlowModelRepository,
|
|
30
34
|
ModelConstructor,
|
|
@@ -35,6 +39,8 @@ import type {
|
|
|
35
39
|
} from './types';
|
|
36
40
|
import { isInheritedFrom } from './utils';
|
|
37
41
|
|
|
42
|
+
const getFlowEngineLoggerLevel = () => (process.env.NODE_ENV === 'production' ? 'warn' : 'trace');
|
|
43
|
+
|
|
38
44
|
/**
|
|
39
45
|
* FlowEngine is the core class of the flow engine, responsible for managing flow models, actions, model repository, and more.
|
|
40
46
|
* It provides capabilities for registering, creating, finding, persisting, replacing, and moving models.
|
|
@@ -75,6 +81,32 @@ export class FlowEngine {
|
|
|
75
81
|
*/
|
|
76
82
|
private _modelClasses: Map<string, ModelConstructor> = observable.shallow(new Map());
|
|
77
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Registered model entries.
|
|
86
|
+
* Key is the model class name, value is the model loader entry.
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
private _modelLoaders: Map<string, FlowModelLoaderEntry> = new Map();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* In-flight model loading promises.
|
|
93
|
+
* Key is the model class name, value is the loading promise.
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
private _loadingModelPromises: Map<string, Promise<ModelConstructor | null>> = new Map();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether model-loader preload has completed in this session.
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
private _modelLoadersPreloaded = false;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* In-flight model-loader preload promise.
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
private _modelLoadersPreloadPromise?: Promise<EnsureBatchResult>;
|
|
109
|
+
|
|
78
110
|
/**
|
|
79
111
|
* Created model instances.
|
|
80
112
|
* Key is the model instance UID, value is the model instance object.
|
|
@@ -117,6 +149,13 @@ export class FlowEngine {
|
|
|
117
149
|
private _previousEngine?: FlowEngine;
|
|
118
150
|
private _nextEngine?: FlowEngine;
|
|
119
151
|
|
|
152
|
+
/**
|
|
153
|
+
* 视图销毁回调。由 useDrawer / useDialog 在创建弹窗视图时注册,
|
|
154
|
+
* 供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
|
|
155
|
+
* embed 视图(usePage)不注册此回调,因此 destroyView() 会自然跳过。
|
|
156
|
+
*/
|
|
157
|
+
private _destroyView?: () => void;
|
|
158
|
+
|
|
120
159
|
private _resources = new Map<string, typeof FlowResource>();
|
|
121
160
|
|
|
122
161
|
/**
|
|
@@ -176,7 +215,7 @@ export class FlowEngine {
|
|
|
176
215
|
MultiRecordResource,
|
|
177
216
|
});
|
|
178
217
|
this.logger = pino({
|
|
179
|
-
level:
|
|
218
|
+
level: getFlowEngineLoggerLevel(),
|
|
180
219
|
browser: {
|
|
181
220
|
write: {
|
|
182
221
|
fatal: (o) => console.trace(o),
|
|
@@ -282,6 +321,28 @@ export class FlowEngine {
|
|
|
282
321
|
}
|
|
283
322
|
}
|
|
284
323
|
|
|
324
|
+
/**
|
|
325
|
+
* 注册视图销毁回调(由 useDrawer / useDialog 调用)。
|
|
326
|
+
*/
|
|
327
|
+
public setDestroyView(fn: () => void): void {
|
|
328
|
+
this._destroyView = fn;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 关闭当前引擎关联的弹窗视图。
|
|
333
|
+
* 路由触发的弹窗会先 navigation.back() 清理 URL,再 destroy() 移除元素;
|
|
334
|
+
* 非路由弹窗直接 destroy()。
|
|
335
|
+
* embed 视图不注册回调,调用时返回 false 自动跳过。
|
|
336
|
+
* @returns 是否成功执行
|
|
337
|
+
*/
|
|
338
|
+
public destroyView(): boolean {
|
|
339
|
+
if (this._destroyView) {
|
|
340
|
+
this._destroyView();
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
285
346
|
// (已移除)getModelGlobal/forEachModelGlobal/getAllModelsGlobal:不再维护冗余全局遍历 API
|
|
286
347
|
|
|
287
348
|
/**
|
|
@@ -395,6 +456,13 @@ export class FlowEngine {
|
|
|
395
456
|
* @private
|
|
396
457
|
*/
|
|
397
458
|
#registerModel(name: string, modelClass: ModelConstructor): void {
|
|
459
|
+
return this._registerModel(name, modelClass);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* for proxy instance, the #registerModel can't be called.
|
|
464
|
+
*/
|
|
465
|
+
private _registerModel(name: string, modelClass: ModelConstructor): void {
|
|
398
466
|
if (this._modelClasses.has(name)) {
|
|
399
467
|
console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
|
|
400
468
|
}
|
|
@@ -415,6 +483,306 @@ export class FlowEngine {
|
|
|
415
483
|
}
|
|
416
484
|
}
|
|
417
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Register multiple model loader entries.
|
|
488
|
+
* The `extends` field declares parent class(es) for async subclass discovery via `getSubclassesOfAsync`.
|
|
489
|
+
* It accepts `string | ModelConstructor | (string | ModelConstructor)[]` and is normalized to `string[]` internally.
|
|
490
|
+
* @param {FlowModelLoaderInputMap} loaders Model loader input map
|
|
491
|
+
* @returns {void}
|
|
492
|
+
* @example
|
|
493
|
+
* flowEngine.registerModelLoaders({
|
|
494
|
+
* DemoModel: {
|
|
495
|
+
* extends: 'BaseModel',
|
|
496
|
+
* loader: () => import('./models/DemoModel'),
|
|
497
|
+
* },
|
|
498
|
+
* });
|
|
499
|
+
*/
|
|
500
|
+
public registerModelLoaders(loaders: FlowModelLoaderInputMap): void {
|
|
501
|
+
let changed = false;
|
|
502
|
+
for (const [name, input] of Object.entries(loaders)) {
|
|
503
|
+
if (this._modelLoaders.has(name)) {
|
|
504
|
+
console.warn(`FlowEngine: Model loader with name '${name}' is already registered and will be overwritten.`);
|
|
505
|
+
}
|
|
506
|
+
const entry: FlowModelLoaderEntry = {
|
|
507
|
+
loader: input.loader,
|
|
508
|
+
};
|
|
509
|
+
if (input.extends != null) {
|
|
510
|
+
const raw = Array.isArray(input.extends) ? input.extends : [input.extends];
|
|
511
|
+
entry.extends = raw.map((item) => (typeof item === 'string' ? item : item.name));
|
|
512
|
+
}
|
|
513
|
+
this._modelLoaders.set(name, entry);
|
|
514
|
+
changed = true;
|
|
515
|
+
}
|
|
516
|
+
if (changed) {
|
|
517
|
+
this._modelLoadersPreloaded = false;
|
|
518
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get a registered model class (constructor) asynchronously.
|
|
524
|
+
* This will first ensure the model loader entry is resolved.
|
|
525
|
+
* @param {string} name Model class name
|
|
526
|
+
* @returns {Promise<ModelConstructor | undefined>} Model constructor, or undefined if not found
|
|
527
|
+
*/
|
|
528
|
+
public async getModelClassAsync(name: string): Promise<ModelConstructor | undefined> {
|
|
529
|
+
await this.ensureModel(name);
|
|
530
|
+
return this.getModelClass(name);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get all registered model classes asynchronously.
|
|
535
|
+
* This will first ensure all registered model loader entries are resolved.
|
|
536
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model class map
|
|
537
|
+
*/
|
|
538
|
+
public async getModelClassesAsync(): Promise<Map<string, ModelConstructor>> {
|
|
539
|
+
await this.ensureModels(Array.from(this._modelLoaders.keys()));
|
|
540
|
+
return this.getModelClasses();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Create and register a model instance asynchronously.
|
|
545
|
+
* This will first ensure all string-based model references in the model tree are resolved.
|
|
546
|
+
* @template T FlowModel subclass type, defaults to FlowModel.
|
|
547
|
+
* @param {CreateModelOptions} options Model creation options
|
|
548
|
+
* @returns {Promise<T>} Created model instance
|
|
549
|
+
*/
|
|
550
|
+
public async createModelAsync<T extends FlowModel = FlowModel>(
|
|
551
|
+
options: CreateModelOptions,
|
|
552
|
+
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
553
|
+
): Promise<T> {
|
|
554
|
+
await this.resolveModelTree(options);
|
|
555
|
+
return this.createModel<T>(options, extra);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Normalize a loader result into a model constructor.
|
|
560
|
+
* @param {string} name Model class name
|
|
561
|
+
* @param {FlowModelLoaderResult} loaded Loader result
|
|
562
|
+
* @returns {ModelConstructor | null} Normalized model constructor
|
|
563
|
+
* @private
|
|
564
|
+
*/
|
|
565
|
+
private normalizeModelLoaderResult(name: string, loaded: FlowModelLoaderResult): ModelConstructor | null {
|
|
566
|
+
if (typeof loaded === 'function') {
|
|
567
|
+
return loaded as ModelConstructor;
|
|
568
|
+
}
|
|
569
|
+
if (loaded && typeof loaded === 'object') {
|
|
570
|
+
const defaultExport = loaded.default;
|
|
571
|
+
if (typeof defaultExport === 'function') {
|
|
572
|
+
return defaultExport as ModelConstructor;
|
|
573
|
+
}
|
|
574
|
+
const namedExport = loaded[name];
|
|
575
|
+
if (typeof namedExport === 'function') {
|
|
576
|
+
return namedExport as ModelConstructor;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
console.warn(`FlowEngine: model loader for '${name}' did not resolve to a valid model constructor.`);
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Collect string-based model names from a model tree.
|
|
585
|
+
* @param {unknown} data Model tree data
|
|
586
|
+
* @param {Set<string>} names Model name set
|
|
587
|
+
* @private
|
|
588
|
+
*/
|
|
589
|
+
private collectModelNamesFromTree(data: unknown, names: Set<string>): void {
|
|
590
|
+
if (!data || typeof data !== 'object') {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (Array.isArray(data)) {
|
|
594
|
+
data.forEach((item) => this.collectModelNamesFromTree(item, names));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const tree = data as Record<string, any>;
|
|
599
|
+
if (typeof tree.use === 'string') {
|
|
600
|
+
names.add(tree.use);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const subModels = tree.subModels;
|
|
604
|
+
if (!subModels || typeof subModels !== 'object') {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
Object.values(subModels).forEach((value) => {
|
|
609
|
+
this.collectModelNamesFromTree(value, names);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Collect additional model names from object-form meta.createModelOptions defaults.
|
|
615
|
+
* @param {ModelConstructor} modelClass Model class constructor
|
|
616
|
+
* @param {Set<string>} names Model name set
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
private collectModelNamesFromMetaDefaults(modelClass: ModelConstructor, names: Set<string>): void {
|
|
620
|
+
const metaCreate = (modelClass as typeof FlowModel).meta?.createModelOptions;
|
|
621
|
+
if (metaCreate && typeof metaCreate === 'object') {
|
|
622
|
+
this.collectModelNamesFromTree(metaCreate, names);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Ensure a single model class is available.
|
|
628
|
+
* @param {string} name Model class name
|
|
629
|
+
* @returns {Promise<ModelConstructor | null>} Model constructor or null when resolution fails
|
|
630
|
+
* @private
|
|
631
|
+
*/
|
|
632
|
+
private async ensureModel(name: string): Promise<ModelConstructor | null> {
|
|
633
|
+
const existing = this._modelClasses.get(name);
|
|
634
|
+
if (existing) {
|
|
635
|
+
return existing;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const inflight = this._loadingModelPromises.get(name);
|
|
639
|
+
if (inflight) {
|
|
640
|
+
return inflight;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const entry = this._modelLoaders.get(name);
|
|
644
|
+
if (!entry) {
|
|
645
|
+
console.warn(`FlowEngine: Model entry '${name}' not found. Falling back to ErrorFlowModel when needed.`);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const promise = (async () => {
|
|
650
|
+
try {
|
|
651
|
+
const loaded = await entry.loader();
|
|
652
|
+
const modelClass = this.normalizeModelLoaderResult(name, loaded);
|
|
653
|
+
if (!modelClass) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
// 这里拿到的 this 是 Proxy(FlowEngine) 而不是原始的 FlowEngine,无法直接调用 #registerModel
|
|
657
|
+
this._registerModel(name, modelClass);
|
|
658
|
+
return modelClass;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
console.warn(`FlowEngine: Failed to load model '${name}'. Falling back to ErrorFlowModel when needed.`, error);
|
|
661
|
+
return null;
|
|
662
|
+
} finally {
|
|
663
|
+
this._loadingModelPromises.delete(name);
|
|
664
|
+
}
|
|
665
|
+
})();
|
|
666
|
+
|
|
667
|
+
this._loadingModelPromises.set(name, promise);
|
|
668
|
+
return promise;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Ensure multiple model classes are available.
|
|
673
|
+
* @param {string[]} names Model class names
|
|
674
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
675
|
+
* @private
|
|
676
|
+
*/
|
|
677
|
+
private async ensureModels(names: string[]): Promise<EnsureBatchResult> {
|
|
678
|
+
const requested = Array.from(new Set(names.filter((name): name is string => !!name)));
|
|
679
|
+
const loaded: string[] = [];
|
|
680
|
+
const failed: EnsureBatchResult['failed'] = [];
|
|
681
|
+
|
|
682
|
+
const results = await Promise.all(
|
|
683
|
+
requested.map(async (name) => {
|
|
684
|
+
const modelClass = await this.ensureModel(name);
|
|
685
|
+
return { name, modelClass };
|
|
686
|
+
}),
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
results.forEach(({ name, modelClass }) => {
|
|
690
|
+
if (modelClass) {
|
|
691
|
+
loaded.push(name);
|
|
692
|
+
} else {
|
|
693
|
+
failed.push({ name });
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
return { requested, loaded, failed };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Resolve all unresolved string-based model references in a model tree before synchronous creation begins.
|
|
702
|
+
*
|
|
703
|
+
* Use this when you already have a model tree object, such as repository-returned data or resolved
|
|
704
|
+
* `createModelOptions`, and you need to ensure every string `use` in that tree has been loaded and
|
|
705
|
+
* registered into `_modelClasses` before calling `createModel()`.
|
|
706
|
+
*
|
|
707
|
+
* @param {unknown} data Model tree data
|
|
708
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
709
|
+
*/
|
|
710
|
+
public async resolveModelTree(data: unknown): Promise<EnsureBatchResult> {
|
|
711
|
+
const requested = new Set<string>();
|
|
712
|
+
const loaded = new Set<string>();
|
|
713
|
+
const failed = new Map<string, { name: string; error?: unknown }>();
|
|
714
|
+
const processed = new Set<string>();
|
|
715
|
+
const pending = new Set<string>();
|
|
716
|
+
|
|
717
|
+
this.collectModelNamesFromTree(data, pending);
|
|
718
|
+
|
|
719
|
+
while (pending.size > 0) {
|
|
720
|
+
const batch = Array.from(pending).filter((name) => !processed.has(name));
|
|
721
|
+
pending.clear();
|
|
722
|
+
if (batch.length === 0) {
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
batch.forEach((name) => requested.add(name));
|
|
727
|
+
const result = await this.ensureModels(batch);
|
|
728
|
+
|
|
729
|
+
result.loaded.forEach((name) => {
|
|
730
|
+
processed.add(name);
|
|
731
|
+
loaded.add(name);
|
|
732
|
+
const modelClass = this.getModelClass(name);
|
|
733
|
+
if (modelClass) {
|
|
734
|
+
const discovered = new Set<string>();
|
|
735
|
+
this.collectModelNamesFromMetaDefaults(modelClass, discovered);
|
|
736
|
+
discovered.forEach((discoveredName) => {
|
|
737
|
+
if (!processed.has(discoveredName)) {
|
|
738
|
+
pending.add(discoveredName);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
result.failed.forEach((item) => {
|
|
745
|
+
processed.add(item.name);
|
|
746
|
+
failed.set(item.name, item);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
requested: Array.from(requested),
|
|
752
|
+
loaded: Array.from(loaded),
|
|
753
|
+
failed: Array.from(failed.values()),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Preload all currently registered unresolved model loaders.
|
|
759
|
+
*
|
|
760
|
+
* This method is intended for flow-settings/discovery style entry points that need registered model
|
|
761
|
+
* classes to exist before UI is rendered, without requiring callers to know which specific models
|
|
762
|
+
* will be touched next.
|
|
763
|
+
*
|
|
764
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
765
|
+
*/
|
|
766
|
+
public async preloadModelLoaders(): Promise<EnsureBatchResult> {
|
|
767
|
+
const unresolved = Array.from(this._modelLoaders.keys()).filter((name) => !this._modelClasses.has(name));
|
|
768
|
+
if (unresolved.length === 0) {
|
|
769
|
+
this._modelLoadersPreloaded = true;
|
|
770
|
+
return { requested: [], loaded: [], failed: [] };
|
|
771
|
+
}
|
|
772
|
+
if (this._modelLoadersPreloadPromise) {
|
|
773
|
+
return this._modelLoadersPreloadPromise;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this._modelLoadersPreloadPromise = (async () => {
|
|
777
|
+
const result = await this.ensureModels(unresolved);
|
|
778
|
+
this._modelLoadersPreloaded = result.failed.length === 0;
|
|
779
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
780
|
+
return result;
|
|
781
|
+
})();
|
|
782
|
+
|
|
783
|
+
return this._modelLoadersPreloadPromise;
|
|
784
|
+
}
|
|
785
|
+
|
|
418
786
|
registerResources(resources: Record<string, any>) {
|
|
419
787
|
for (const [name, resourceClass] of Object.entries(resources)) {
|
|
420
788
|
this._resources.set(name, resourceClass);
|
|
@@ -489,6 +857,70 @@ export class FlowEngine {
|
|
|
489
857
|
return result;
|
|
490
858
|
}
|
|
491
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Asynchronously get all subclasses of a base class, including those registered via model loaders.
|
|
862
|
+
* Merges results from already-loaded classes (_modelClasses) and async loader entries with matching `extends` declarations.
|
|
863
|
+
* Loader-resolved classes are validated with `isInheritedFrom`; mismatches are warned and excluded.
|
|
864
|
+
* @param {string | ModelConstructor} baseClass Base class name or constructor
|
|
865
|
+
* @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function
|
|
866
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model classes that are subclasses of the base class
|
|
867
|
+
*/
|
|
868
|
+
public async getSubclassesOfAsync(
|
|
869
|
+
baseClass: string | ModelConstructor,
|
|
870
|
+
filter?: (ModelClass: ModelConstructor, className: string) => boolean,
|
|
871
|
+
): Promise<Map<string, ModelConstructor>> {
|
|
872
|
+
const baseClassName = typeof baseClass === 'string' ? baseClass : baseClass.name;
|
|
873
|
+
|
|
874
|
+
// If baseClass is a string and not yet loaded, try to resolve it first
|
|
875
|
+
let parentModelClass: ModelConstructor | undefined;
|
|
876
|
+
if (typeof baseClass === 'string') {
|
|
877
|
+
if (!this.getModelClass(baseClass)) {
|
|
878
|
+
await this.ensureModel(baseClass);
|
|
879
|
+
}
|
|
880
|
+
parentModelClass = this.getModelClass(baseClass);
|
|
881
|
+
} else {
|
|
882
|
+
parentModelClass = baseClass;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (!parentModelClass) {
|
|
886
|
+
return new Map();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Step 1: Collect already-loaded subclasses from _modelClasses
|
|
890
|
+
const result = this.getSubclassesOf(parentModelClass, filter);
|
|
891
|
+
|
|
892
|
+
// Step 2: Find unloaded loaders whose extends includes baseClassName
|
|
893
|
+
const loaderCandidates: string[] = [];
|
|
894
|
+
for (const [name, entry] of this._modelLoaders) {
|
|
895
|
+
if (result.has(name) || this._modelClasses.has(name)) continue;
|
|
896
|
+
if (entry.extends?.includes(baseClassName)) {
|
|
897
|
+
loaderCandidates.push(name);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Step 3: Resolve all matching loaders
|
|
902
|
+
if (loaderCandidates.length > 0) {
|
|
903
|
+
await this.ensureModels(loaderCandidates);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Step 4: Validate resolved classes and add to result
|
|
907
|
+
for (const name of loaderCandidates) {
|
|
908
|
+
const ModelClass = this._modelClasses.get(name);
|
|
909
|
+
if (!ModelClass) continue;
|
|
910
|
+
if (!isInheritedFrom(ModelClass, parentModelClass)) {
|
|
911
|
+
console.warn(
|
|
912
|
+
`FlowEngine: Model '${name}' declares extends '${baseClassName}' but does not actually inherit from it. Skipping.`,
|
|
913
|
+
);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (!filter || filter(ModelClass, name)) {
|
|
917
|
+
result.set(name, ModelClass);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return result;
|
|
922
|
+
}
|
|
923
|
+
|
|
492
924
|
/**
|
|
493
925
|
* Create and register a model instance.
|
|
494
926
|
* If an instance with the same UID exists, returns the existing instance.
|
|
@@ -579,7 +1011,6 @@ export class FlowEngine {
|
|
|
579
1011
|
|
|
580
1012
|
while (current) {
|
|
581
1013
|
if (visited.has(current)) {
|
|
582
|
-
console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
|
|
583
1014
|
break;
|
|
584
1015
|
}
|
|
585
1016
|
visited.add(current);
|
|
@@ -698,7 +1129,7 @@ export class FlowEngine {
|
|
|
698
1129
|
*/
|
|
699
1130
|
public removeModel(uid: string): boolean {
|
|
700
1131
|
if (!this._modelInstances.has(uid)) {
|
|
701
|
-
|
|
1132
|
+
this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
|
|
702
1133
|
return false;
|
|
703
1134
|
}
|
|
704
1135
|
const modelInstance = this._modelInstances.get(uid) as FlowModel;
|
|
@@ -836,10 +1267,10 @@ export class FlowEngine {
|
|
|
836
1267
|
* Hydrate a model into current engine from an already-existing model instance in previous engines.
|
|
837
1268
|
* - Avoids repository requests when the model tree is already present in memory.
|
|
838
1269
|
*/
|
|
839
|
-
private hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
1270
|
+
private async hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
840
1271
|
options: any,
|
|
841
1272
|
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
842
|
-
): T | null {
|
|
1273
|
+
): Promise<T | null> {
|
|
843
1274
|
const uid = options?.uid;
|
|
844
1275
|
const parentId = options?.parentId;
|
|
845
1276
|
const subKey = options?.subKey;
|
|
@@ -853,7 +1284,7 @@ export class FlowEngine {
|
|
|
853
1284
|
}
|
|
854
1285
|
if (existing) {
|
|
855
1286
|
const data = existing.serialize();
|
|
856
|
-
return this.
|
|
1287
|
+
return this.createModelAsync<T>(data as any, extra);
|
|
857
1288
|
}
|
|
858
1289
|
}
|
|
859
1290
|
|
|
@@ -868,11 +1299,11 @@ export class FlowEngine {
|
|
|
868
1299
|
if (!localParent) {
|
|
869
1300
|
const parentData = parentFromPrev.serialize();
|
|
870
1301
|
delete (parentData as any).subModels;
|
|
871
|
-
localParent = this.
|
|
1302
|
+
localParent = await this.createModelAsync<FlowModel>(parentData as any, extra);
|
|
872
1303
|
}
|
|
873
1304
|
// Create (or reuse) the sub-model instance in current engine.
|
|
874
1305
|
const modelData = modelFromPrev.serialize();
|
|
875
|
-
const localModel = this.
|
|
1306
|
+
const localModel = await this.createModelAsync<T>(modelData as any, extra);
|
|
876
1307
|
|
|
877
1308
|
// Mount under local parent if not mounted yet (so later lookups by parentId/subKey won't hit repo).
|
|
878
1309
|
const mounted = (localParent.subModels as any)?.[subKey];
|
|
@@ -913,20 +1344,21 @@ export class FlowEngine {
|
|
|
913
1344
|
if (model) {
|
|
914
1345
|
return model as T;
|
|
915
1346
|
}
|
|
916
|
-
const hydrated = this.hydrateModelFromPreviousEngines<T>(options);
|
|
1347
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options);
|
|
917
1348
|
if (hydrated) {
|
|
918
1349
|
return hydrated as T;
|
|
919
1350
|
}
|
|
920
1351
|
}
|
|
921
1352
|
const data = await this._modelRepository.findOne(options);
|
|
922
1353
|
if (!data?.uid) return null;
|
|
1354
|
+
await this.resolveModelTree(data);
|
|
923
1355
|
if (refresh) {
|
|
924
1356
|
const existing = this.getModel(data.uid);
|
|
925
1357
|
if (existing) {
|
|
926
1358
|
this.removeModelWithSubModels(existing.uid);
|
|
927
1359
|
}
|
|
928
1360
|
}
|
|
929
|
-
return this.
|
|
1361
|
+
return this.createModelAsync<T>(data as any);
|
|
930
1362
|
}
|
|
931
1363
|
|
|
932
1364
|
/**
|
|
@@ -959,6 +1391,7 @@ export class FlowEngine {
|
|
|
959
1391
|
async loadOrCreateModel<T extends FlowModel = FlowModel>(
|
|
960
1392
|
options,
|
|
961
1393
|
extra?: {
|
|
1394
|
+
skipSave?: boolean;
|
|
962
1395
|
delegateToParent?: boolean;
|
|
963
1396
|
delegate?: FlowContext;
|
|
964
1397
|
},
|
|
@@ -973,7 +1406,7 @@ export class FlowEngine {
|
|
|
973
1406
|
return m;
|
|
974
1407
|
}
|
|
975
1408
|
|
|
976
|
-
const hydrated = this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1409
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
977
1410
|
if (hydrated) {
|
|
978
1411
|
return hydrated;
|
|
979
1412
|
}
|
|
@@ -981,10 +1414,12 @@ export class FlowEngine {
|
|
|
981
1414
|
const data = await this._modelRepository.findOne(options);
|
|
982
1415
|
let model: T | null = null;
|
|
983
1416
|
if (data?.uid) {
|
|
984
|
-
model = this.
|
|
1417
|
+
model = await this.createModelAsync<T>(data as any, extra);
|
|
985
1418
|
} else {
|
|
986
|
-
model = this.
|
|
987
|
-
|
|
1419
|
+
model = await this.createModelAsync<T>(options, extra);
|
|
1420
|
+
if (!extra?.skipSave) {
|
|
1421
|
+
await model.save();
|
|
1422
|
+
}
|
|
988
1423
|
}
|
|
989
1424
|
if (model.parent) {
|
|
990
1425
|
const subModel = model.parent.findSubModel(model.subKey, (m) => {
|
package/src/flowI18n.ts
CHANGED
|
@@ -52,7 +52,8 @@ export class FlowI18n {
|
|
|
52
52
|
*/
|
|
53
53
|
private translateKey(key: string, options?: any): string {
|
|
54
54
|
if (this.context?.i18n?.t) {
|
|
55
|
-
|
|
55
|
+
const translated = this.context.i18n.t(key, options);
|
|
56
|
+
return translated == null || translated === '' ? key : translated;
|
|
56
57
|
}
|
|
57
58
|
// 如果没有翻译函数,返回原始键值
|
|
58
59
|
return key;
|