@nocobase/flow-engine 2.1.0-beta.15 → 2.1.0-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/lib/components/MobilePopup.js +6 -5
  2. package/lib/components/subModel/AddSubModelButton.js +1 -1
  3. package/lib/components/subModel/utils.js +2 -2
  4. package/lib/flowEngine.d.ts +132 -1
  5. package/lib/flowEngine.js +360 -14
  6. package/lib/flowSettings.d.ts +14 -6
  7. package/lib/flowSettings.js +34 -6
  8. package/lib/lazy-helper.d.ts +14 -0
  9. package/lib/lazy-helper.js +71 -0
  10. package/lib/models/flowModel.js +17 -7
  11. package/lib/types.d.ts +46 -0
  12. package/lib/utils/runjsTemplateCompat.js +1 -1
  13. package/package.json +4 -4
  14. package/src/__tests__/flow-engine.test.ts +166 -0
  15. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  16. package/src/__tests__/flowSettings.test.ts +94 -15
  17. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  18. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  19. package/src/components/MobilePopup.tsx +4 -2
  20. package/src/components/__tests__/FlowModelRenderer.test.tsx +22 -0
  21. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
  22. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +6 -6
  23. package/src/components/subModel/AddSubModelButton.tsx +1 -1
  24. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +93 -33
  25. package/src/components/subModel/utils.ts +1 -1
  26. package/src/flowEngine.ts +412 -10
  27. package/src/flowSettings.ts +40 -6
  28. package/src/lazy-helper.tsx +57 -0
  29. package/src/models/flowModel.tsx +18 -6
  30. package/src/types.ts +59 -0
  31. 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.createModel<T>(data as any, extra);
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.createModel<FlowModel>(parentData as any, extra);
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.createModel<T>(modelData as any, extra);
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.createModel<T>(data as any);
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.createModel<T>(data as any, extra);
1416
+ model = await this.createModelAsync<T>(data as any, extra);
1015
1417
  } else {
1016
- model = this.createModel<T>(options, extra);
1418
+ model = await this.createModelAsync<T>(options, extra);
1017
1419
  if (!extra?.skipSave) {
1018
1420
  await model.save();
1019
1421
  }
@@ -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
+ }