@nocobase/flow-engine 2.1.0-beta.15 → 2.1.0-beta.16
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/components/MobilePopup.js +6 -5
- package/lib/components/subModel/AddSubModelButton.js +1 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/flowEngine.d.ts +132 -1
- package/lib/flowEngine.js +360 -14
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/models/flowModel.js +17 -7
- package/lib/types.d.ts +46 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/package.json +4 -4
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +22 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +6 -6
- package/src/components/subModel/AddSubModelButton.tsx +1 -1
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +93 -33
- package/src/components/subModel/utils.ts +1 -1
- package/src/flowEngine.ts +412 -10
- package/src/flowSettings.ts +40 -6
- package/src/lazy-helper.tsx +57 -0
- package/src/models/flowModel.tsx +18 -6
- package/src/types.ts +59 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
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,
|
|
@@ -75,6 +79,32 @@ export class FlowEngine {
|
|
|
75
79
|
*/
|
|
76
80
|
private _modelClasses: Map<string, ModelConstructor> = observable.shallow(new Map());
|
|
77
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Registered model entries.
|
|
84
|
+
* Key is the model class name, value is the model loader entry.
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
private _modelLoaders: Map<string, FlowModelLoaderEntry> = new Map();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* In-flight model loading promises.
|
|
91
|
+
* Key is the model class name, value is the loading promise.
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
private _loadingModelPromises: Map<string, Promise<ModelConstructor | null>> = new Map();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether model-loader preload has completed in this session.
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
private _modelLoadersPreloaded = false;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* In-flight model-loader preload promise.
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
private _modelLoadersPreloadPromise?: Promise<EnsureBatchResult>;
|
|
107
|
+
|
|
78
108
|
/**
|
|
79
109
|
* Created model instances.
|
|
80
110
|
* Key is the model instance UID, value is the model instance object.
|
|
@@ -424,6 +454,13 @@ export class FlowEngine {
|
|
|
424
454
|
* @private
|
|
425
455
|
*/
|
|
426
456
|
#registerModel(name: string, modelClass: ModelConstructor): void {
|
|
457
|
+
return this._registerModel(name, modelClass);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* for proxy instance, the #registerModel can't be called.
|
|
462
|
+
*/
|
|
463
|
+
private _registerModel(name: string, modelClass: ModelConstructor): void {
|
|
427
464
|
if (this._modelClasses.has(name)) {
|
|
428
465
|
console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`);
|
|
429
466
|
}
|
|
@@ -444,6 +481,306 @@ export class FlowEngine {
|
|
|
444
481
|
}
|
|
445
482
|
}
|
|
446
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Register multiple model loader entries.
|
|
486
|
+
* The `extends` field declares parent class(es) for async subclass discovery via `getSubclassesOfAsync`.
|
|
487
|
+
* It accepts `string | ModelConstructor | (string | ModelConstructor)[]` and is normalized to `string[]` internally.
|
|
488
|
+
* @param {FlowModelLoaderInputMap} loaders Model loader input map
|
|
489
|
+
* @returns {void}
|
|
490
|
+
* @example
|
|
491
|
+
* flowEngine.registerModelLoaders({
|
|
492
|
+
* DemoModel: {
|
|
493
|
+
* extends: 'BaseModel',
|
|
494
|
+
* loader: () => import('./models/DemoModel'),
|
|
495
|
+
* },
|
|
496
|
+
* });
|
|
497
|
+
*/
|
|
498
|
+
public registerModelLoaders(loaders: FlowModelLoaderInputMap): void {
|
|
499
|
+
let changed = false;
|
|
500
|
+
for (const [name, input] of Object.entries(loaders)) {
|
|
501
|
+
if (this._modelLoaders.has(name)) {
|
|
502
|
+
console.warn(`FlowEngine: Model loader with name '${name}' is already registered and will be overwritten.`);
|
|
503
|
+
}
|
|
504
|
+
const entry: FlowModelLoaderEntry = {
|
|
505
|
+
loader: input.loader,
|
|
506
|
+
};
|
|
507
|
+
if (input.extends != null) {
|
|
508
|
+
const raw = Array.isArray(input.extends) ? input.extends : [input.extends];
|
|
509
|
+
entry.extends = raw.map((item) => (typeof item === 'string' ? item : item.name));
|
|
510
|
+
}
|
|
511
|
+
this._modelLoaders.set(name, entry);
|
|
512
|
+
changed = true;
|
|
513
|
+
}
|
|
514
|
+
if (changed) {
|
|
515
|
+
this._modelLoadersPreloaded = false;
|
|
516
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Get a registered model class (constructor) asynchronously.
|
|
522
|
+
* This will first ensure the model loader entry is resolved.
|
|
523
|
+
* @param {string} name Model class name
|
|
524
|
+
* @returns {Promise<ModelConstructor | undefined>} Model constructor, or undefined if not found
|
|
525
|
+
*/
|
|
526
|
+
public async getModelClassAsync(name: string): Promise<ModelConstructor | undefined> {
|
|
527
|
+
await this.ensureModel(name);
|
|
528
|
+
return this.getModelClass(name);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Get all registered model classes asynchronously.
|
|
533
|
+
* This will first ensure all registered model loader entries are resolved.
|
|
534
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model class map
|
|
535
|
+
*/
|
|
536
|
+
public async getModelClassesAsync(): Promise<Map<string, ModelConstructor>> {
|
|
537
|
+
await this.ensureModels(Array.from(this._modelLoaders.keys()));
|
|
538
|
+
return this.getModelClasses();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Create and register a model instance asynchronously.
|
|
543
|
+
* This will first ensure all string-based model references in the model tree are resolved.
|
|
544
|
+
* @template T FlowModel subclass type, defaults to FlowModel.
|
|
545
|
+
* @param {CreateModelOptions} options Model creation options
|
|
546
|
+
* @returns {Promise<T>} Created model instance
|
|
547
|
+
*/
|
|
548
|
+
public async createModelAsync<T extends FlowModel = FlowModel>(
|
|
549
|
+
options: CreateModelOptions,
|
|
550
|
+
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
551
|
+
): Promise<T> {
|
|
552
|
+
await this.resolveModelTree(options);
|
|
553
|
+
return this.createModel<T>(options, extra);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Normalize a loader result into a model constructor.
|
|
558
|
+
* @param {string} name Model class name
|
|
559
|
+
* @param {FlowModelLoaderResult} loaded Loader result
|
|
560
|
+
* @returns {ModelConstructor | null} Normalized model constructor
|
|
561
|
+
* @private
|
|
562
|
+
*/
|
|
563
|
+
private normalizeModelLoaderResult(name: string, loaded: FlowModelLoaderResult): ModelConstructor | null {
|
|
564
|
+
if (typeof loaded === 'function') {
|
|
565
|
+
return loaded as ModelConstructor;
|
|
566
|
+
}
|
|
567
|
+
if (loaded && typeof loaded === 'object') {
|
|
568
|
+
const defaultExport = loaded.default;
|
|
569
|
+
if (typeof defaultExport === 'function') {
|
|
570
|
+
return defaultExport as ModelConstructor;
|
|
571
|
+
}
|
|
572
|
+
const namedExport = loaded[name];
|
|
573
|
+
if (typeof namedExport === 'function') {
|
|
574
|
+
return namedExport as ModelConstructor;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
console.warn(`FlowEngine: model loader for '${name}' did not resolve to a valid model constructor.`);
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Collect string-based model names from a model tree.
|
|
583
|
+
* @param {unknown} data Model tree data
|
|
584
|
+
* @param {Set<string>} names Model name set
|
|
585
|
+
* @private
|
|
586
|
+
*/
|
|
587
|
+
private collectModelNamesFromTree(data: unknown, names: Set<string>): void {
|
|
588
|
+
if (!data || typeof data !== 'object') {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (Array.isArray(data)) {
|
|
592
|
+
data.forEach((item) => this.collectModelNamesFromTree(item, names));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const tree = data as Record<string, any>;
|
|
597
|
+
if (typeof tree.use === 'string') {
|
|
598
|
+
names.add(tree.use);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const subModels = tree.subModels;
|
|
602
|
+
if (!subModels || typeof subModels !== 'object') {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
Object.values(subModels).forEach((value) => {
|
|
607
|
+
this.collectModelNamesFromTree(value, names);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Collect additional model names from object-form meta.createModelOptions defaults.
|
|
613
|
+
* @param {ModelConstructor} modelClass Model class constructor
|
|
614
|
+
* @param {Set<string>} names Model name set
|
|
615
|
+
* @private
|
|
616
|
+
*/
|
|
617
|
+
private collectModelNamesFromMetaDefaults(modelClass: ModelConstructor, names: Set<string>): void {
|
|
618
|
+
const metaCreate = (modelClass as typeof FlowModel).meta?.createModelOptions;
|
|
619
|
+
if (metaCreate && typeof metaCreate === 'object') {
|
|
620
|
+
this.collectModelNamesFromTree(metaCreate, names);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Ensure a single model class is available.
|
|
626
|
+
* @param {string} name Model class name
|
|
627
|
+
* @returns {Promise<ModelConstructor | null>} Model constructor or null when resolution fails
|
|
628
|
+
* @private
|
|
629
|
+
*/
|
|
630
|
+
private async ensureModel(name: string): Promise<ModelConstructor | null> {
|
|
631
|
+
const existing = this._modelClasses.get(name);
|
|
632
|
+
if (existing) {
|
|
633
|
+
return existing;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const inflight = this._loadingModelPromises.get(name);
|
|
637
|
+
if (inflight) {
|
|
638
|
+
return inflight;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const entry = this._modelLoaders.get(name);
|
|
642
|
+
if (!entry) {
|
|
643
|
+
console.warn(`FlowEngine: Model entry '${name}' not found. Falling back to ErrorFlowModel when needed.`);
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const promise = (async () => {
|
|
648
|
+
try {
|
|
649
|
+
const loaded = await entry.loader();
|
|
650
|
+
const modelClass = this.normalizeModelLoaderResult(name, loaded);
|
|
651
|
+
if (!modelClass) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
// 这里拿到的 this 是 Proxy(FlowEngine) 而不是原始的 FlowEngine,无法直接调用 #registerModel
|
|
655
|
+
this._registerModel(name, modelClass);
|
|
656
|
+
return modelClass;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.warn(`FlowEngine: Failed to load model '${name}'. Falling back to ErrorFlowModel when needed.`, error);
|
|
659
|
+
return null;
|
|
660
|
+
} finally {
|
|
661
|
+
this._loadingModelPromises.delete(name);
|
|
662
|
+
}
|
|
663
|
+
})();
|
|
664
|
+
|
|
665
|
+
this._loadingModelPromises.set(name, promise);
|
|
666
|
+
return promise;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Ensure multiple model classes are available.
|
|
671
|
+
* @param {string[]} names Model class names
|
|
672
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
673
|
+
* @private
|
|
674
|
+
*/
|
|
675
|
+
private async ensureModels(names: string[]): Promise<EnsureBatchResult> {
|
|
676
|
+
const requested = Array.from(new Set(names.filter((name): name is string => !!name)));
|
|
677
|
+
const loaded: string[] = [];
|
|
678
|
+
const failed: EnsureBatchResult['failed'] = [];
|
|
679
|
+
|
|
680
|
+
const results = await Promise.all(
|
|
681
|
+
requested.map(async (name) => {
|
|
682
|
+
const modelClass = await this.ensureModel(name);
|
|
683
|
+
return { name, modelClass };
|
|
684
|
+
}),
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
results.forEach(({ name, modelClass }) => {
|
|
688
|
+
if (modelClass) {
|
|
689
|
+
loaded.push(name);
|
|
690
|
+
} else {
|
|
691
|
+
failed.push({ name });
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
return { requested, loaded, failed };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Resolve all unresolved string-based model references in a model tree before synchronous creation begins.
|
|
700
|
+
*
|
|
701
|
+
* Use this when you already have a model tree object, such as repository-returned data or resolved
|
|
702
|
+
* `createModelOptions`, and you need to ensure every string `use` in that tree has been loaded and
|
|
703
|
+
* registered into `_modelClasses` before calling `createModel()`.
|
|
704
|
+
*
|
|
705
|
+
* @param {unknown} data Model tree data
|
|
706
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
707
|
+
*/
|
|
708
|
+
public async resolveModelTree(data: unknown): Promise<EnsureBatchResult> {
|
|
709
|
+
const requested = new Set<string>();
|
|
710
|
+
const loaded = new Set<string>();
|
|
711
|
+
const failed = new Map<string, { name: string; error?: unknown }>();
|
|
712
|
+
const processed = new Set<string>();
|
|
713
|
+
const pending = new Set<string>();
|
|
714
|
+
|
|
715
|
+
this.collectModelNamesFromTree(data, pending);
|
|
716
|
+
|
|
717
|
+
while (pending.size > 0) {
|
|
718
|
+
const batch = Array.from(pending).filter((name) => !processed.has(name));
|
|
719
|
+
pending.clear();
|
|
720
|
+
if (batch.length === 0) {
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
batch.forEach((name) => requested.add(name));
|
|
725
|
+
const result = await this.ensureModels(batch);
|
|
726
|
+
|
|
727
|
+
result.loaded.forEach((name) => {
|
|
728
|
+
processed.add(name);
|
|
729
|
+
loaded.add(name);
|
|
730
|
+
const modelClass = this.getModelClass(name);
|
|
731
|
+
if (modelClass) {
|
|
732
|
+
const discovered = new Set<string>();
|
|
733
|
+
this.collectModelNamesFromMetaDefaults(modelClass, discovered);
|
|
734
|
+
discovered.forEach((discoveredName) => {
|
|
735
|
+
if (!processed.has(discoveredName)) {
|
|
736
|
+
pending.add(discoveredName);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
result.failed.forEach((item) => {
|
|
743
|
+
processed.add(item.name);
|
|
744
|
+
failed.set(item.name, item);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
requested: Array.from(requested),
|
|
750
|
+
loaded: Array.from(loaded),
|
|
751
|
+
failed: Array.from(failed.values()),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Preload all currently registered unresolved model loaders.
|
|
757
|
+
*
|
|
758
|
+
* This method is intended for flow-settings/discovery style entry points that need registered model
|
|
759
|
+
* classes to exist before UI is rendered, without requiring callers to know which specific models
|
|
760
|
+
* will be touched next.
|
|
761
|
+
*
|
|
762
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
763
|
+
*/
|
|
764
|
+
public async preloadModelLoaders(): Promise<EnsureBatchResult> {
|
|
765
|
+
const unresolved = Array.from(this._modelLoaders.keys()).filter((name) => !this._modelClasses.has(name));
|
|
766
|
+
if (unresolved.length === 0) {
|
|
767
|
+
this._modelLoadersPreloaded = true;
|
|
768
|
+
return { requested: [], loaded: [], failed: [] };
|
|
769
|
+
}
|
|
770
|
+
if (this._modelLoadersPreloadPromise) {
|
|
771
|
+
return this._modelLoadersPreloadPromise;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
this._modelLoadersPreloadPromise = (async () => {
|
|
775
|
+
const result = await this.ensureModels(unresolved);
|
|
776
|
+
this._modelLoadersPreloaded = result.failed.length === 0;
|
|
777
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
778
|
+
return result;
|
|
779
|
+
})();
|
|
780
|
+
|
|
781
|
+
return this._modelLoadersPreloadPromise;
|
|
782
|
+
}
|
|
783
|
+
|
|
447
784
|
registerResources(resources: Record<string, any>) {
|
|
448
785
|
for (const [name, resourceClass] of Object.entries(resources)) {
|
|
449
786
|
this._resources.set(name, resourceClass);
|
|
@@ -518,6 +855,70 @@ export class FlowEngine {
|
|
|
518
855
|
return result;
|
|
519
856
|
}
|
|
520
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Asynchronously get all subclasses of a base class, including those registered via model loaders.
|
|
860
|
+
* Merges results from already-loaded classes (_modelClasses) and async loader entries with matching `extends` declarations.
|
|
861
|
+
* Loader-resolved classes are validated with `isInheritedFrom`; mismatches are warned and excluded.
|
|
862
|
+
* @param {string | ModelConstructor} baseClass Base class name or constructor
|
|
863
|
+
* @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function
|
|
864
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model classes that are subclasses of the base class
|
|
865
|
+
*/
|
|
866
|
+
public async getSubclassesOfAsync(
|
|
867
|
+
baseClass: string | ModelConstructor,
|
|
868
|
+
filter?: (ModelClass: ModelConstructor, className: string) => boolean,
|
|
869
|
+
): Promise<Map<string, ModelConstructor>> {
|
|
870
|
+
const baseClassName = typeof baseClass === 'string' ? baseClass : baseClass.name;
|
|
871
|
+
|
|
872
|
+
// If baseClass is a string and not yet loaded, try to resolve it first
|
|
873
|
+
let parentModelClass: ModelConstructor | undefined;
|
|
874
|
+
if (typeof baseClass === 'string') {
|
|
875
|
+
if (!this.getModelClass(baseClass)) {
|
|
876
|
+
await this.ensureModel(baseClass);
|
|
877
|
+
}
|
|
878
|
+
parentModelClass = this.getModelClass(baseClass);
|
|
879
|
+
} else {
|
|
880
|
+
parentModelClass = baseClass;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!parentModelClass) {
|
|
884
|
+
return new Map();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Step 1: Collect already-loaded subclasses from _modelClasses
|
|
888
|
+
const result = this.getSubclassesOf(parentModelClass, filter);
|
|
889
|
+
|
|
890
|
+
// Step 2: Find unloaded loaders whose extends includes baseClassName
|
|
891
|
+
const loaderCandidates: string[] = [];
|
|
892
|
+
for (const [name, entry] of this._modelLoaders) {
|
|
893
|
+
if (result.has(name) || this._modelClasses.has(name)) continue;
|
|
894
|
+
if (entry.extends?.includes(baseClassName)) {
|
|
895
|
+
loaderCandidates.push(name);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Step 3: Resolve all matching loaders
|
|
900
|
+
if (loaderCandidates.length > 0) {
|
|
901
|
+
await this.ensureModels(loaderCandidates);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Step 4: Validate resolved classes and add to result
|
|
905
|
+
for (const name of loaderCandidates) {
|
|
906
|
+
const ModelClass = this._modelClasses.get(name);
|
|
907
|
+
if (!ModelClass) continue;
|
|
908
|
+
if (!isInheritedFrom(ModelClass, parentModelClass)) {
|
|
909
|
+
console.warn(
|
|
910
|
+
`FlowEngine: Model '${name}' declares extends '${baseClassName}' but does not actually inherit from it. Skipping.`,
|
|
911
|
+
);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (!filter || filter(ModelClass, name)) {
|
|
915
|
+
result.set(name, ModelClass);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
|
|
521
922
|
/**
|
|
522
923
|
* Create and register a model instance.
|
|
523
924
|
* If an instance with the same UID exists, returns the existing instance.
|
|
@@ -865,10 +1266,10 @@ export class FlowEngine {
|
|
|
865
1266
|
* Hydrate a model into current engine from an already-existing model instance in previous engines.
|
|
866
1267
|
* - Avoids repository requests when the model tree is already present in memory.
|
|
867
1268
|
*/
|
|
868
|
-
private hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
1269
|
+
private async hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
869
1270
|
options: any,
|
|
870
1271
|
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
871
|
-
): T | null {
|
|
1272
|
+
): Promise<T | null> {
|
|
872
1273
|
const uid = options?.uid;
|
|
873
1274
|
const parentId = options?.parentId;
|
|
874
1275
|
const subKey = options?.subKey;
|
|
@@ -882,7 +1283,7 @@ export class FlowEngine {
|
|
|
882
1283
|
}
|
|
883
1284
|
if (existing) {
|
|
884
1285
|
const data = existing.serialize();
|
|
885
|
-
return this.
|
|
1286
|
+
return this.createModelAsync<T>(data as any, extra);
|
|
886
1287
|
}
|
|
887
1288
|
}
|
|
888
1289
|
|
|
@@ -897,11 +1298,11 @@ export class FlowEngine {
|
|
|
897
1298
|
if (!localParent) {
|
|
898
1299
|
const parentData = parentFromPrev.serialize();
|
|
899
1300
|
delete (parentData as any).subModels;
|
|
900
|
-
localParent = this.
|
|
1301
|
+
localParent = await this.createModelAsync<FlowModel>(parentData as any, extra);
|
|
901
1302
|
}
|
|
902
1303
|
// Create (or reuse) the sub-model instance in current engine.
|
|
903
1304
|
const modelData = modelFromPrev.serialize();
|
|
904
|
-
const localModel = this.
|
|
1305
|
+
const localModel = await this.createModelAsync<T>(modelData as any, extra);
|
|
905
1306
|
|
|
906
1307
|
// Mount under local parent if not mounted yet (so later lookups by parentId/subKey won't hit repo).
|
|
907
1308
|
const mounted = (localParent.subModels as any)?.[subKey];
|
|
@@ -942,20 +1343,21 @@ export class FlowEngine {
|
|
|
942
1343
|
if (model) {
|
|
943
1344
|
return model as T;
|
|
944
1345
|
}
|
|
945
|
-
const hydrated = this.hydrateModelFromPreviousEngines<T>(options);
|
|
1346
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options);
|
|
946
1347
|
if (hydrated) {
|
|
947
1348
|
return hydrated as T;
|
|
948
1349
|
}
|
|
949
1350
|
}
|
|
950
1351
|
const data = await this._modelRepository.findOne(options);
|
|
951
1352
|
if (!data?.uid) return null;
|
|
1353
|
+
await this.resolveModelTree(data);
|
|
952
1354
|
if (refresh) {
|
|
953
1355
|
const existing = this.getModel(data.uid);
|
|
954
1356
|
if (existing) {
|
|
955
1357
|
this.removeModelWithSubModels(existing.uid);
|
|
956
1358
|
}
|
|
957
1359
|
}
|
|
958
|
-
return this.
|
|
1360
|
+
return this.createModelAsync<T>(data as any);
|
|
959
1361
|
}
|
|
960
1362
|
|
|
961
1363
|
/**
|
|
@@ -1003,7 +1405,7 @@ export class FlowEngine {
|
|
|
1003
1405
|
return m;
|
|
1004
1406
|
}
|
|
1005
1407
|
|
|
1006
|
-
const hydrated = this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1408
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1007
1409
|
if (hydrated) {
|
|
1008
1410
|
return hydrated;
|
|
1009
1411
|
}
|
|
@@ -1011,9 +1413,9 @@ export class FlowEngine {
|
|
|
1011
1413
|
const data = await this._modelRepository.findOne(options);
|
|
1012
1414
|
let model: T | null = null;
|
|
1013
1415
|
if (data?.uid) {
|
|
1014
|
-
model = this.
|
|
1416
|
+
model = await this.createModelAsync<T>(data as any, extra);
|
|
1015
1417
|
} else {
|
|
1016
|
-
model = this.
|
|
1418
|
+
model = await this.createModelAsync<T>(options, extra);
|
|
1017
1419
|
if (!extra?.skipSave) {
|
|
1018
1420
|
await model.save();
|
|
1019
1421
|
}
|
package/src/flowSettings.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import { FlowExitAllException } from './utils/exceptions';
|
|
36
36
|
import { FlowStepContext } from './hooks/useFlowStep';
|
|
37
37
|
import { GLOBAL_EMBED_CONTAINER_ID, EMBED_REPLACING_DATA_KEY } from './views';
|
|
38
|
+
import { lazy } from './lazy-helper';
|
|
38
39
|
|
|
39
40
|
const Panel = Collapse.Panel;
|
|
40
41
|
|
|
@@ -114,16 +115,23 @@ export interface FlowSettingsOpenOptions {
|
|
|
114
115
|
onSaved?: () => void | Promise<void>;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
export type FlowSettingsComponent = React.ComponentType<any>;
|
|
119
|
+
export type FlowSettingsComponentModule = { default?: FlowSettingsComponent } | Record<string, FlowSettingsComponent>;
|
|
120
|
+
export type FlowSettingsComponentLoader = () => Promise<FlowSettingsComponentModule | FlowSettingsComponent>;
|
|
121
|
+
export type FlowSettingsComponentLoaderMap = Record<string, FlowSettingsComponentLoader>;
|
|
122
|
+
|
|
117
123
|
export class FlowSettings {
|
|
118
124
|
public components: Record<string, any> = {};
|
|
119
125
|
public scopes: Record<string, any> = {};
|
|
120
126
|
private antdComponentsLoaded = false;
|
|
121
127
|
public enabled: boolean;
|
|
128
|
+
private engine: FlowEngine;
|
|
122
129
|
#forceEnabled = false; // 强制启用状态,主要用于设计模式下的强制启用
|
|
123
130
|
public toolbarItems: ToolbarItemConfig[] = [];
|
|
124
131
|
#emitter: Emitter = new Emitter();
|
|
125
132
|
|
|
126
133
|
constructor(engine: FlowEngine) {
|
|
134
|
+
this.engine = engine;
|
|
127
135
|
// 初始默认为 false,由 SchemaComponentProvider 根据实际设计模式状态同步设置
|
|
128
136
|
this.enabled = false;
|
|
129
137
|
engine.context.defineProperty('flowSettingsEnabled', {
|
|
@@ -291,6 +299,30 @@ export class FlowSettings {
|
|
|
291
299
|
});
|
|
292
300
|
}
|
|
293
301
|
|
|
302
|
+
public registerComponentLoaders(loaders: FlowSettingsComponentLoaderMap): void {
|
|
303
|
+
Object.entries(loaders).forEach(([name, loader]) => {
|
|
304
|
+
if (this.components[name]) {
|
|
305
|
+
console.warn(`FlowSettings: Component with name '${name}' is already registered and will be overwritten.`);
|
|
306
|
+
}
|
|
307
|
+
this.components[name] = lazy(async () => {
|
|
308
|
+
const loaded = await loader();
|
|
309
|
+
if (typeof loaded === 'function') {
|
|
310
|
+
return { default: loaded };
|
|
311
|
+
}
|
|
312
|
+
if (loaded?.default && typeof loaded.default === 'function') {
|
|
313
|
+
return { default: loaded.default };
|
|
314
|
+
}
|
|
315
|
+
const namedComponent = loaded?.[name];
|
|
316
|
+
if (typeof namedComponent === 'function') {
|
|
317
|
+
return { default: namedComponent };
|
|
318
|
+
}
|
|
319
|
+
throw new Error(
|
|
320
|
+
`FlowSettings: component loader for '${name}' must resolve to a React component or a module exporting it.`,
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
294
326
|
/**
|
|
295
327
|
* 添加作用域到 FlowSettings 的作用域注册表中。
|
|
296
328
|
* 这些作用域可以在 flow step 的 uiSchema 中使用。
|
|
@@ -311,13 +343,15 @@ export class FlowSettings {
|
|
|
311
343
|
/**
|
|
312
344
|
* 启用流程设置组件的显示
|
|
313
345
|
* @example
|
|
314
|
-
* flowSettings.enable();
|
|
346
|
+
* await flowSettings.enable();
|
|
315
347
|
*/
|
|
316
|
-
public enable(): void {
|
|
348
|
+
public async enable(): Promise<void> {
|
|
349
|
+
await this.engine.preloadModelLoaders();
|
|
317
350
|
this.enabled = true;
|
|
318
351
|
}
|
|
319
352
|
|
|
320
|
-
public forceEnable() {
|
|
353
|
+
public async forceEnable(): Promise<void> {
|
|
354
|
+
await this.engine.preloadModelLoaders();
|
|
321
355
|
this.#forceEnabled = true;
|
|
322
356
|
this.enabled = true;
|
|
323
357
|
}
|
|
@@ -325,16 +359,16 @@ export class FlowSettings {
|
|
|
325
359
|
/**
|
|
326
360
|
* 禁用流程设置组件的显示
|
|
327
361
|
* @example
|
|
328
|
-
* flowSettings.disable();
|
|
362
|
+
* await flowSettings.disable();
|
|
329
363
|
*/
|
|
330
|
-
public disable(): void {
|
|
364
|
+
public async disable(): Promise<void> {
|
|
331
365
|
if (this.#forceEnabled) {
|
|
332
366
|
return;
|
|
333
367
|
}
|
|
334
368
|
this.enabled = false;
|
|
335
369
|
}
|
|
336
370
|
|
|
337
|
-
public forceDisable() {
|
|
371
|
+
public async forceDisable(): Promise<void> {
|
|
338
372
|
this.#forceEnabled = false;
|
|
339
373
|
this.enabled = false;
|
|
340
374
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { lazy as reactLazy } from 'react';
|
|
11
|
+
|
|
12
|
+
type LazyComponentType<M extends Record<string, any>, K extends keyof M> = {
|
|
13
|
+
[P in K]: M[P];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function lazy<M extends Record<'default', any>>(factory: () => Promise<M>): M['default'];
|
|
17
|
+
|
|
18
|
+
export function lazy<M extends Record<string, any>, K extends keyof M = keyof M>(
|
|
19
|
+
factory: () => Promise<M>,
|
|
20
|
+
...componentNames: K[]
|
|
21
|
+
): LazyComponentType<M, K>;
|
|
22
|
+
|
|
23
|
+
export function lazy<M extends Record<string, any>, K extends keyof M>(
|
|
24
|
+
factory: () => Promise<M>,
|
|
25
|
+
...componentNames: K[]
|
|
26
|
+
) {
|
|
27
|
+
if (componentNames.length === 0) {
|
|
28
|
+
const LazyComponent = reactLazy(() =>
|
|
29
|
+
factory().then((module) => ({
|
|
30
|
+
default: module.default,
|
|
31
|
+
})),
|
|
32
|
+
);
|
|
33
|
+
const Component = (props) => (
|
|
34
|
+
<React.Suspense fallback={null}>
|
|
35
|
+
<LazyComponent {...props} />
|
|
36
|
+
</React.Suspense>
|
|
37
|
+
);
|
|
38
|
+
return Component;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return componentNames.reduce(
|
|
42
|
+
(acc, name) => {
|
|
43
|
+
const LazyComponent = reactLazy(() =>
|
|
44
|
+
factory().then((module) => ({
|
|
45
|
+
default: module[name],
|
|
46
|
+
})),
|
|
47
|
+
);
|
|
48
|
+
acc[name] = ((props) => (
|
|
49
|
+
<React.Suspense fallback={null}>
|
|
50
|
+
<LazyComponent {...props} />
|
|
51
|
+
</React.Suspense>
|
|
52
|
+
)) as M[K];
|
|
53
|
+
return acc;
|
|
54
|
+
},
|
|
55
|
+
{} as LazyComponentType<M, K>,
|
|
56
|
+
);
|
|
57
|
+
}
|