@nocobase/flow-engine 2.0.22 → 2.1.0-alpha.10
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 +120 -1
- package/lib/flowEngine.js +301 -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 +35 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/package.json +4 -4
- 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__/flow-model-render-error-fallback.test.tsx +3 -3
- 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 +338 -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 +47 -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
|
+
FlowModelLoaderMap,
|
|
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,296 @@ export class FlowEngine {
|
|
|
444
481
|
}
|
|
445
482
|
}
|
|
446
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Register multiple model loader entries.
|
|
486
|
+
* @param {FlowModelLoaderMap} loaders Model loader entry map, key is model name, value is the model loader entry
|
|
487
|
+
* @returns {void}
|
|
488
|
+
* @example
|
|
489
|
+
* flowEngine.registerModelLoaders({
|
|
490
|
+
* DemoModel: {
|
|
491
|
+
* loader: () => import('./models/DemoModel'),
|
|
492
|
+
* },
|
|
493
|
+
* });
|
|
494
|
+
*/
|
|
495
|
+
public registerModelLoaders(loaders: FlowModelLoaderMap): void {
|
|
496
|
+
let changed = false;
|
|
497
|
+
for (const [name, entry] of Object.entries(loaders)) {
|
|
498
|
+
if (this._modelLoaders.has(name)) {
|
|
499
|
+
console.warn(`FlowEngine: Model loader with name '${name}' is already registered and will be overwritten.`);
|
|
500
|
+
}
|
|
501
|
+
this._modelLoaders.set(name, entry);
|
|
502
|
+
changed = true;
|
|
503
|
+
}
|
|
504
|
+
if (changed) {
|
|
505
|
+
this._modelLoadersPreloaded = false;
|
|
506
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Get a registered model class (constructor) asynchronously.
|
|
512
|
+
* This will first ensure the model loader entry is resolved.
|
|
513
|
+
* @param {string} name Model class name
|
|
514
|
+
* @returns {Promise<ModelConstructor | undefined>} Model constructor, or undefined if not found
|
|
515
|
+
*/
|
|
516
|
+
public async getModelClassAsync(name: string): Promise<ModelConstructor | undefined> {
|
|
517
|
+
await this.ensureModel(name);
|
|
518
|
+
return this.getModelClass(name);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get all registered model classes asynchronously.
|
|
523
|
+
* This will first ensure all registered model loader entries are resolved.
|
|
524
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model class map
|
|
525
|
+
*/
|
|
526
|
+
public async getModelClassesAsync(): Promise<Map<string, ModelConstructor>> {
|
|
527
|
+
await this.ensureModels(Array.from(this._modelLoaders.keys()));
|
|
528
|
+
return this.getModelClasses();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Create and register a model instance asynchronously.
|
|
533
|
+
* This will first ensure all string-based model references in the model tree are resolved.
|
|
534
|
+
* @template T FlowModel subclass type, defaults to FlowModel.
|
|
535
|
+
* @param {CreateModelOptions} options Model creation options
|
|
536
|
+
* @returns {Promise<T>} Created model instance
|
|
537
|
+
*/
|
|
538
|
+
public async createModelAsync<T extends FlowModel = FlowModel>(
|
|
539
|
+
options: CreateModelOptions,
|
|
540
|
+
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
541
|
+
): Promise<T> {
|
|
542
|
+
await this.resolveModelTree(options);
|
|
543
|
+
return this.createModel<T>(options, extra);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Normalize a loader result into a model constructor.
|
|
548
|
+
* @param {string} name Model class name
|
|
549
|
+
* @param {FlowModelLoaderResult} loaded Loader result
|
|
550
|
+
* @returns {ModelConstructor | null} Normalized model constructor
|
|
551
|
+
* @private
|
|
552
|
+
*/
|
|
553
|
+
private normalizeModelLoaderResult(name: string, loaded: FlowModelLoaderResult): ModelConstructor | null {
|
|
554
|
+
if (typeof loaded === 'function') {
|
|
555
|
+
return loaded as ModelConstructor;
|
|
556
|
+
}
|
|
557
|
+
if (loaded && typeof loaded === 'object') {
|
|
558
|
+
const defaultExport = loaded.default;
|
|
559
|
+
if (typeof defaultExport === 'function') {
|
|
560
|
+
return defaultExport as ModelConstructor;
|
|
561
|
+
}
|
|
562
|
+
const namedExport = loaded[name];
|
|
563
|
+
if (typeof namedExport === 'function') {
|
|
564
|
+
return namedExport as ModelConstructor;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
console.warn(`FlowEngine: model loader for '${name}' did not resolve to a valid model constructor.`);
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Collect string-based model names from a model tree.
|
|
573
|
+
* @param {unknown} data Model tree data
|
|
574
|
+
* @param {Set<string>} names Model name set
|
|
575
|
+
* @private
|
|
576
|
+
*/
|
|
577
|
+
private collectModelNamesFromTree(data: unknown, names: Set<string>): void {
|
|
578
|
+
if (!data || typeof data !== 'object') {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (Array.isArray(data)) {
|
|
582
|
+
data.forEach((item) => this.collectModelNamesFromTree(item, names));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const tree = data as Record<string, any>;
|
|
587
|
+
if (typeof tree.use === 'string') {
|
|
588
|
+
names.add(tree.use);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const subModels = tree.subModels;
|
|
592
|
+
if (!subModels || typeof subModels !== 'object') {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
Object.values(subModels).forEach((value) => {
|
|
597
|
+
this.collectModelNamesFromTree(value, names);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Collect additional model names from object-form meta.createModelOptions defaults.
|
|
603
|
+
* @param {ModelConstructor} modelClass Model class constructor
|
|
604
|
+
* @param {Set<string>} names Model name set
|
|
605
|
+
* @private
|
|
606
|
+
*/
|
|
607
|
+
private collectModelNamesFromMetaDefaults(modelClass: ModelConstructor, names: Set<string>): void {
|
|
608
|
+
const metaCreate = (modelClass as typeof FlowModel).meta?.createModelOptions;
|
|
609
|
+
if (metaCreate && typeof metaCreate === 'object') {
|
|
610
|
+
this.collectModelNamesFromTree(metaCreate, names);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Ensure a single model class is available.
|
|
616
|
+
* @param {string} name Model class name
|
|
617
|
+
* @returns {Promise<ModelConstructor | null>} Model constructor or null when resolution fails
|
|
618
|
+
* @private
|
|
619
|
+
*/
|
|
620
|
+
private async ensureModel(name: string): Promise<ModelConstructor | null> {
|
|
621
|
+
const existing = this._modelClasses.get(name);
|
|
622
|
+
if (existing) {
|
|
623
|
+
return existing;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const inflight = this._loadingModelPromises.get(name);
|
|
627
|
+
if (inflight) {
|
|
628
|
+
return inflight;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const entry = this._modelLoaders.get(name);
|
|
632
|
+
if (!entry) {
|
|
633
|
+
console.warn(`FlowEngine: Model entry '${name}' not found. Falling back to ErrorFlowModel when needed.`);
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const promise = (async () => {
|
|
638
|
+
try {
|
|
639
|
+
const loaded = await entry.loader();
|
|
640
|
+
const modelClass = this.normalizeModelLoaderResult(name, loaded);
|
|
641
|
+
if (!modelClass) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
// 这里拿到的 this 是 Proxy(FlowEngine) 而不是原始的 FlowEngine,无法直接调用 #registerModel
|
|
645
|
+
this._registerModel(name, modelClass);
|
|
646
|
+
return modelClass;
|
|
647
|
+
} catch (error) {
|
|
648
|
+
console.warn(`FlowEngine: Failed to load model '${name}'. Falling back to ErrorFlowModel when needed.`, error);
|
|
649
|
+
return null;
|
|
650
|
+
} finally {
|
|
651
|
+
this._loadingModelPromises.delete(name);
|
|
652
|
+
}
|
|
653
|
+
})();
|
|
654
|
+
|
|
655
|
+
this._loadingModelPromises.set(name, promise);
|
|
656
|
+
return promise;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Ensure multiple model classes are available.
|
|
661
|
+
* @param {string[]} names Model class names
|
|
662
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
663
|
+
* @private
|
|
664
|
+
*/
|
|
665
|
+
private async ensureModels(names: string[]): Promise<EnsureBatchResult> {
|
|
666
|
+
const requested = Array.from(new Set(names.filter((name): name is string => !!name)));
|
|
667
|
+
const loaded: string[] = [];
|
|
668
|
+
const failed: EnsureBatchResult['failed'] = [];
|
|
669
|
+
|
|
670
|
+
const results = await Promise.all(
|
|
671
|
+
requested.map(async (name) => {
|
|
672
|
+
const modelClass = await this.ensureModel(name);
|
|
673
|
+
return { name, modelClass };
|
|
674
|
+
}),
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
results.forEach(({ name, modelClass }) => {
|
|
678
|
+
if (modelClass) {
|
|
679
|
+
loaded.push(name);
|
|
680
|
+
} else {
|
|
681
|
+
failed.push({ name });
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
return { requested, loaded, failed };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Resolve all unresolved string-based model references in a model tree before synchronous creation begins.
|
|
690
|
+
*
|
|
691
|
+
* Use this when you already have a model tree object, such as repository-returned data or resolved
|
|
692
|
+
* `createModelOptions`, and you need to ensure every string `use` in that tree has been loaded and
|
|
693
|
+
* registered into `_modelClasses` before calling `createModel()`.
|
|
694
|
+
*
|
|
695
|
+
* @param {unknown} data Model tree data
|
|
696
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
697
|
+
*/
|
|
698
|
+
public async resolveModelTree(data: unknown): Promise<EnsureBatchResult> {
|
|
699
|
+
const requested = new Set<string>();
|
|
700
|
+
const loaded = new Set<string>();
|
|
701
|
+
const failed = new Map<string, { name: string; error?: unknown }>();
|
|
702
|
+
const processed = new Set<string>();
|
|
703
|
+
const pending = new Set<string>();
|
|
704
|
+
|
|
705
|
+
this.collectModelNamesFromTree(data, pending);
|
|
706
|
+
|
|
707
|
+
while (pending.size > 0) {
|
|
708
|
+
const batch = Array.from(pending).filter((name) => !processed.has(name));
|
|
709
|
+
pending.clear();
|
|
710
|
+
if (batch.length === 0) {
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
batch.forEach((name) => requested.add(name));
|
|
715
|
+
const result = await this.ensureModels(batch);
|
|
716
|
+
|
|
717
|
+
result.loaded.forEach((name) => {
|
|
718
|
+
processed.add(name);
|
|
719
|
+
loaded.add(name);
|
|
720
|
+
const modelClass = this.getModelClass(name);
|
|
721
|
+
if (modelClass) {
|
|
722
|
+
const discovered = new Set<string>();
|
|
723
|
+
this.collectModelNamesFromMetaDefaults(modelClass, discovered);
|
|
724
|
+
discovered.forEach((discoveredName) => {
|
|
725
|
+
if (!processed.has(discoveredName)) {
|
|
726
|
+
pending.add(discoveredName);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
result.failed.forEach((item) => {
|
|
733
|
+
processed.add(item.name);
|
|
734
|
+
failed.set(item.name, item);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
requested: Array.from(requested),
|
|
740
|
+
loaded: Array.from(loaded),
|
|
741
|
+
failed: Array.from(failed.values()),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Preload all currently registered unresolved model loaders.
|
|
747
|
+
*
|
|
748
|
+
* This method is intended for flow-settings/discovery style entry points that need registered model
|
|
749
|
+
* classes to exist before UI is rendered, without requiring callers to know which specific models
|
|
750
|
+
* will be touched next.
|
|
751
|
+
*
|
|
752
|
+
* @returns {Promise<EnsureBatchResult>} Batch ensure result
|
|
753
|
+
*/
|
|
754
|
+
public async preloadModelLoaders(): Promise<EnsureBatchResult> {
|
|
755
|
+
const unresolved = Array.from(this._modelLoaders.keys()).filter((name) => !this._modelClasses.has(name));
|
|
756
|
+
if (unresolved.length === 0) {
|
|
757
|
+
this._modelLoadersPreloaded = true;
|
|
758
|
+
return { requested: [], loaded: [], failed: [] };
|
|
759
|
+
}
|
|
760
|
+
if (this._modelLoadersPreloadPromise) {
|
|
761
|
+
return this._modelLoadersPreloadPromise;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this._modelLoadersPreloadPromise = (async () => {
|
|
765
|
+
const result = await this.ensureModels(unresolved);
|
|
766
|
+
this._modelLoadersPreloaded = result.failed.length === 0;
|
|
767
|
+
this._modelLoadersPreloadPromise = undefined;
|
|
768
|
+
return result;
|
|
769
|
+
})();
|
|
770
|
+
|
|
771
|
+
return this._modelLoadersPreloadPromise;
|
|
772
|
+
}
|
|
773
|
+
|
|
447
774
|
registerResources(resources: Record<string, any>) {
|
|
448
775
|
for (const [name, resourceClass] of Object.entries(resources)) {
|
|
449
776
|
this._resources.set(name, resourceClass);
|
|
@@ -865,10 +1192,10 @@ export class FlowEngine {
|
|
|
865
1192
|
* Hydrate a model into current engine from an already-existing model instance in previous engines.
|
|
866
1193
|
* - Avoids repository requests when the model tree is already present in memory.
|
|
867
1194
|
*/
|
|
868
|
-
private hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
1195
|
+
private async hydrateModelFromPreviousEngines<T extends FlowModel = FlowModel>(
|
|
869
1196
|
options: any,
|
|
870
1197
|
extra?: { delegateToParent?: boolean; delegate?: FlowContext },
|
|
871
|
-
): T | null {
|
|
1198
|
+
): Promise<T | null> {
|
|
872
1199
|
const uid = options?.uid;
|
|
873
1200
|
const parentId = options?.parentId;
|
|
874
1201
|
const subKey = options?.subKey;
|
|
@@ -882,7 +1209,7 @@ export class FlowEngine {
|
|
|
882
1209
|
}
|
|
883
1210
|
if (existing) {
|
|
884
1211
|
const data = existing.serialize();
|
|
885
|
-
return this.
|
|
1212
|
+
return this.createModelAsync<T>(data as any, extra);
|
|
886
1213
|
}
|
|
887
1214
|
}
|
|
888
1215
|
|
|
@@ -897,11 +1224,11 @@ export class FlowEngine {
|
|
|
897
1224
|
if (!localParent) {
|
|
898
1225
|
const parentData = parentFromPrev.serialize();
|
|
899
1226
|
delete (parentData as any).subModels;
|
|
900
|
-
localParent = this.
|
|
1227
|
+
localParent = await this.createModelAsync<FlowModel>(parentData as any, extra);
|
|
901
1228
|
}
|
|
902
1229
|
// Create (or reuse) the sub-model instance in current engine.
|
|
903
1230
|
const modelData = modelFromPrev.serialize();
|
|
904
|
-
const localModel = this.
|
|
1231
|
+
const localModel = await this.createModelAsync<T>(modelData as any, extra);
|
|
905
1232
|
|
|
906
1233
|
// Mount under local parent if not mounted yet (so later lookups by parentId/subKey won't hit repo).
|
|
907
1234
|
const mounted = (localParent.subModels as any)?.[subKey];
|
|
@@ -942,20 +1269,21 @@ export class FlowEngine {
|
|
|
942
1269
|
if (model) {
|
|
943
1270
|
return model as T;
|
|
944
1271
|
}
|
|
945
|
-
const hydrated = this.hydrateModelFromPreviousEngines<T>(options);
|
|
1272
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options);
|
|
946
1273
|
if (hydrated) {
|
|
947
1274
|
return hydrated as T;
|
|
948
1275
|
}
|
|
949
1276
|
}
|
|
950
1277
|
const data = await this._modelRepository.findOne(options);
|
|
951
1278
|
if (!data?.uid) return null;
|
|
1279
|
+
await this.resolveModelTree(data);
|
|
952
1280
|
if (refresh) {
|
|
953
1281
|
const existing = this.getModel(data.uid);
|
|
954
1282
|
if (existing) {
|
|
955
1283
|
this.removeModelWithSubModels(existing.uid);
|
|
956
1284
|
}
|
|
957
1285
|
}
|
|
958
|
-
return this.
|
|
1286
|
+
return this.createModelAsync<T>(data as any);
|
|
959
1287
|
}
|
|
960
1288
|
|
|
961
1289
|
/**
|
|
@@ -1003,7 +1331,7 @@ export class FlowEngine {
|
|
|
1003
1331
|
return m;
|
|
1004
1332
|
}
|
|
1005
1333
|
|
|
1006
|
-
const hydrated = this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1334
|
+
const hydrated = await this.hydrateModelFromPreviousEngines<T>(options, extra);
|
|
1007
1335
|
if (hydrated) {
|
|
1008
1336
|
return hydrated;
|
|
1009
1337
|
}
|
|
@@ -1011,9 +1339,9 @@ export class FlowEngine {
|
|
|
1011
1339
|
const data = await this._modelRepository.findOne(options);
|
|
1012
1340
|
let model: T | null = null;
|
|
1013
1341
|
if (data?.uid) {
|
|
1014
|
-
model = this.
|
|
1342
|
+
model = await this.createModelAsync<T>(data as any, extra);
|
|
1015
1343
|
} else {
|
|
1016
|
-
model = this.
|
|
1344
|
+
model = await this.createModelAsync<T>(options, extra);
|
|
1017
1345
|
if (!extra?.skipSave) {
|
|
1018
1346
|
await model.save();
|
|
1019
1347
|
}
|
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
|
+
}
|
package/src/models/flowModel.tsx
CHANGED
|
@@ -11,8 +11,6 @@ import { batch, define, observable, observe } from '@formily/reactive';
|
|
|
11
11
|
import _ from 'lodash';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { uid } from 'uid/secure';
|
|
14
|
-
import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog';
|
|
15
|
-
import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog';
|
|
16
14
|
import { Emitter } from '../emitter';
|
|
17
15
|
import { InstanceFlowRegistry } from '../flow-registry/InstanceFlowRegistry';
|
|
18
16
|
import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
|
|
@@ -36,7 +34,7 @@ import type {
|
|
|
36
34
|
import { IModelComponentProps, ReadonlyModelProps } from '../types';
|
|
37
35
|
import { isInheritedFrom, setupRuntimeContextSteps } from '../utils';
|
|
38
36
|
// import { FlowExitAllException } from '../utils/exceptions';
|
|
39
|
-
import { Typography } from 'antd
|
|
37
|
+
import { Typography } from 'antd';
|
|
40
38
|
import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
|
|
41
39
|
import { buildSubModelItem } from '../components/subModel/utils';
|
|
42
40
|
import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
|
|
@@ -88,6 +86,16 @@ type ExtraMenuItemEntry = {
|
|
|
88
86
|
|
|
89
87
|
const classMenuExtensions = new WeakMap<typeof FlowModel, Set<ExtraMenuItemEntry>>();
|
|
90
88
|
|
|
89
|
+
async function loadOpenStepSettingsDialog() {
|
|
90
|
+
const mod = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
|
|
91
|
+
return mod.openStepSettingsDialog;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function loadOpenRequiredParamsStepFormDialog() {
|
|
95
|
+
const mod = await import('../components/settings/wrappers/contextual/StepRequiredSettingsDialog');
|
|
96
|
+
return mod.openRequiredParamsStepFormDialog;
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
export enum ModelRenderMode {
|
|
92
100
|
ReactElement = 'reactElement',
|
|
93
101
|
RenderFunction = 'renderFunction',
|
|
@@ -1369,7 +1377,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1369
1377
|
* @param {string} stepKey 步骤的唯一标识符
|
|
1370
1378
|
* @returns {void}
|
|
1371
1379
|
*/
|
|
1372
|
-
openStepSettingsDialog(flowKey: string, stepKey: string) {
|
|
1380
|
+
async openStepSettingsDialog(flowKey: string, stepKey: string) {
|
|
1373
1381
|
// 创建流程运行时上下文
|
|
1374
1382
|
const flow = this.getFlow(flowKey);
|
|
1375
1383
|
const step = flow?.steps?.[stepKey];
|
|
@@ -1383,7 +1391,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1383
1391
|
setupRuntimeContextSteps(ctx, flow.steps, this, flowKey);
|
|
1384
1392
|
ctx.defineProperty('currentStep', { value: step });
|
|
1385
1393
|
|
|
1386
|
-
|
|
1394
|
+
const openStepSettingsDialog = await loadOpenStepSettingsDialog();
|
|
1395
|
+
|
|
1396
|
+
return openStepSettingsDialog({
|
|
1387
1397
|
model: this,
|
|
1388
1398
|
flowKey,
|
|
1389
1399
|
stepKey,
|
|
@@ -1399,7 +1409,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
|
|
|
1399
1409
|
* @returns {Promise<any>} 返回表单提交的值
|
|
1400
1410
|
*/
|
|
1401
1411
|
async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string) {
|
|
1402
|
-
|
|
1412
|
+
const openRequiredParamsStepFormDialog = await loadOpenRequiredParamsStepFormDialog();
|
|
1413
|
+
|
|
1414
|
+
return openRequiredParamsStepFormDialog({
|
|
1403
1415
|
model: this,
|
|
1404
1416
|
dialogWidth,
|
|
1405
1417
|
dialogTitle,
|