@nocobase/flow-engine 2.1.0-beta.22 → 2.1.0-beta.23

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 (45) hide show
  1. package/lib/components/FieldModelRenderer.js +2 -2
  2. package/lib/components/FlowModelRenderer.d.ts +2 -0
  3. package/lib/components/FlowModelRenderer.js +2 -0
  4. package/lib/components/dnd/index.d.ts +19 -1
  5. package/lib/components/dnd/index.js +239 -21
  6. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +20 -1
  7. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +4 -0
  8. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +21 -8
  9. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +2 -0
  10. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +100 -32
  11. package/lib/components/subModel/index.d.ts +1 -0
  12. package/lib/components/subModel/index.js +19 -0
  13. package/lib/components/subModel/utils.d.ts +1 -1
  14. package/lib/data-source/index.d.ts +73 -0
  15. package/lib/data-source/index.js +205 -1
  16. package/lib/flowContext.d.ts +2 -0
  17. package/lib/flowI18n.js +2 -1
  18. package/lib/models/DisplayItemModel.d.ts +1 -1
  19. package/lib/models/EditableItemModel.d.ts +1 -1
  20. package/lib/models/FilterableItemModel.d.ts +1 -1
  21. package/lib/models/flowModel.d.ts +11 -9
  22. package/lib/models/flowModel.js +48 -9
  23. package/lib/provider.js +38 -23
  24. package/package.json +4 -4
  25. package/src/__tests__/provider.test.tsx +24 -2
  26. package/src/components/FieldModelRenderer.tsx +2 -1
  27. package/src/components/FlowModelRenderer.tsx +6 -0
  28. package/src/components/__tests__/dnd.test.ts +44 -0
  29. package/src/components/dnd/index.tsx +286 -26
  30. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +25 -1
  31. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +24 -5
  32. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +94 -3
  33. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +171 -2
  34. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +2 -0
  35. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +112 -32
  36. package/src/components/subModel/index.ts +1 -0
  37. package/src/data-source/__tests__/index.test.ts +34 -1
  38. package/src/data-source/index.ts +252 -2
  39. package/src/flowContext.ts +2 -0
  40. package/src/flowI18n.ts +2 -1
  41. package/src/models/DisplayItemModel.tsx +1 -1
  42. package/src/models/EditableItemModel.tsx +1 -1
  43. package/src/models/FilterableItemModel.tsx +1 -1
  44. package/src/models/flowModel.tsx +85 -23
  45. package/src/provider.tsx +41 -25
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import { observable } from '@formily/reactive';
11
- import { CascaderProps } from 'antd';
12
11
  import _ from 'lodash';
13
12
  import { FlowEngine } from '../flowEngine';
14
13
  import { jioToJoiSchema } from './jioToJoiSchema';
@@ -20,9 +19,31 @@ export interface DataSourceOptions extends Record<string, any> {
20
19
  [key: string]: any;
21
20
  }
22
21
 
22
+ export type DataSourceRequester = (options: Record<string, any>) => Promise<any>;
23
+
24
+ export interface DataSourceLoadResult {
25
+ collections?: CollectionOptions[];
26
+ dataSources?: Array<DataSourceOptions & { collections?: CollectionOptions[] }>;
27
+ }
28
+
29
+ export type DataSourceLoader = (context: { key: string; manager: DataSourceManager }) => Promise<DataSourceLoadResult>;
30
+
23
31
  export class DataSourceManager {
24
32
  dataSources: Map<string, DataSource>;
25
33
  flowEngine: FlowEngine;
34
+ requester?: DataSourceRequester;
35
+ collectionFieldInterfaceManager?: {
36
+ addFieldInterfaces?: (fieldInterfaceClasses?: any[]) => void;
37
+ addFieldInterfaceGroups?: (groups: Record<string, { label: string; order?: number }>) => void;
38
+ addFieldInterfaceComponentOption?: (name: string, option: any) => void;
39
+ addFieldInterfaceOperator?: (name: string, operator: any) => void;
40
+ getFieldInterface?: (name: string) => any;
41
+ };
42
+ loaders = new Map<string, DataSourceLoader>();
43
+ loadedKeys = new Set<string>();
44
+ loadingKeys = new Set<string>();
45
+ loadErrors = new Map<string, Error | null>();
46
+ loadingPromise: Promise<void> | null = null;
26
47
 
27
48
  constructor() {
28
49
  this.dataSources = observable.shallow<Map<string, DataSource>>(new Map());
@@ -32,6 +53,38 @@ export class DataSourceManager {
32
53
  this.flowEngine = flowEngine;
33
54
  }
34
55
 
56
+ setRequester(requester?: DataSourceRequester) {
57
+ this.requester = requester;
58
+ }
59
+
60
+ setCollectionFieldInterfaceManager(manager: DataSourceManager['collectionFieldInterfaceManager']) {
61
+ this.collectionFieldInterfaceManager = manager;
62
+ }
63
+
64
+ addFieldInterfaces(fieldInterfaceClasses: any[] = []) {
65
+ this.collectionFieldInterfaceManager?.addFieldInterfaces?.(fieldInterfaceClasses);
66
+ }
67
+
68
+ addFieldInterfaceGroups(groups: Record<string, { label: string; order?: number }>) {
69
+ this.collectionFieldInterfaceManager?.addFieldInterfaceGroups?.(groups);
70
+ }
71
+
72
+ addFieldInterfaceComponentOption(name: string, option: any) {
73
+ this.collectionFieldInterfaceManager?.addFieldInterfaceComponentOption?.(name, option);
74
+ }
75
+
76
+ addFieldInterfaceOperator(name: string, operator: any) {
77
+ this.collectionFieldInterfaceManager?.addFieldInterfaceOperator?.(name, operator);
78
+ }
79
+
80
+ registerLoader(key: string, loader: DataSourceLoader) {
81
+ this.loaders.set(key, loader);
82
+ }
83
+
84
+ removeLoader(key: string) {
85
+ this.loaders.delete(key);
86
+ }
87
+
35
88
  addDataSource(ds: DataSource | DataSourceOptions) {
36
89
  if (this.dataSources.has(ds.key)) {
37
90
  throw new Error(`DataSource with name ${ds.key} already exists`);
@@ -48,7 +101,7 @@ export class DataSourceManager {
48
101
 
49
102
  upsertDataSource(ds: DataSource | DataSourceOptions) {
50
103
  if (this.dataSources.has(ds.key)) {
51
- this.dataSources.get(ds.key)?.setOptions(ds);
104
+ this.dataSources.get(ds.key)?.patchOptions(ds);
52
105
  } else {
53
106
  this.addDataSource(ds);
54
107
  }
@@ -82,6 +135,164 @@ export class DataSourceManager {
82
135
  if (!ds) return undefined;
83
136
  return ds.getCollectionField(otherKeys.join('.'));
84
137
  }
138
+
139
+ async ensureLoaded(options: { force?: boolean; keys?: string[] } = {}) {
140
+ const { force = false } = options;
141
+ const keys = this.resolveLoadKeys(options.keys);
142
+ const pendingKeys = force ? keys : keys.filter((key) => !this.loadedKeys.has(key));
143
+ if (!pendingKeys.length) {
144
+ return;
145
+ }
146
+ if (this.loadingPromise) {
147
+ return this.loadingPromise;
148
+ }
149
+
150
+ this.loadingPromise = (async () => {
151
+ try {
152
+ for (const key of pendingKeys) {
153
+ await this.loadKey(key, { initial: !this.loadedKeys.has(key), force });
154
+ }
155
+ } finally {
156
+ this.loadingPromise = null;
157
+ }
158
+ })();
159
+
160
+ return this.loadingPromise;
161
+ }
162
+
163
+ async reload(options: { keys?: string[] } = {}) {
164
+ const keys = this.resolveLoadKeys(options.keys);
165
+ if (!keys.length) {
166
+ return;
167
+ }
168
+ if (this.loadingPromise) {
169
+ return this.loadingPromise;
170
+ }
171
+
172
+ this.loadingPromise = (async () => {
173
+ try {
174
+ for (const key of keys) {
175
+ await this.loadKey(key, { initial: false, force: true });
176
+ }
177
+ } finally {
178
+ this.loadingPromise = null;
179
+ }
180
+ })();
181
+
182
+ return this.loadingPromise;
183
+ }
184
+
185
+ async reloadDataSource(key: string) {
186
+ if (this.loadingKeys.has(key) && this.loadingPromise) {
187
+ return this.loadingPromise;
188
+ }
189
+ if (!this.loaders.has(key) && this.loaders.has('*')) {
190
+ return this.reload({ keys: ['*'] });
191
+ }
192
+ return this.reload({ keys: [key] });
193
+ }
194
+
195
+ protected resolveLoadKeys(requestedKeys?: string[]) {
196
+ const normalizedKeys = requestedKeys?.length ? requestedKeys : ['main'];
197
+ const explicitKeys = normalizedKeys.filter((key) => this.loaders.has(key));
198
+ if (this.loaders.has('*')) {
199
+ return _.uniq(['*', ...explicitKeys]);
200
+ }
201
+ return explicitKeys.length ? explicitKeys : normalizedKeys;
202
+ }
203
+
204
+ protected getApp() {
205
+ return this.flowEngine?.context?.app as { eventBus?: EventTarget } | undefined;
206
+ }
207
+
208
+ protected dispatchDataSourceEvent(
209
+ type: 'dataSource:loaded' | 'dataSource:loadFailed',
210
+ detail: { dataSourceKey: string; initial: boolean; error?: Error },
211
+ ) {
212
+ this.getApp()?.eventBus?.dispatchEvent(new CustomEvent(type, { detail }));
213
+ }
214
+
215
+ protected setDataSourceState(
216
+ key: string,
217
+ options: Partial<Pick<DataSourceOptions, 'status' | 'errorMessage'>> & Record<string, any>,
218
+ ) {
219
+ const dataSource = this.getDataSource(key);
220
+ if (!dataSource) {
221
+ return;
222
+ }
223
+ dataSource.patchOptions(options);
224
+ }
225
+
226
+ protected applyDataSourceLoadResult(key: string, result: DataSourceLoadResult) {
227
+ if (key === '*') {
228
+ const dataSources = result?.dataSources || [];
229
+ dataSources.forEach((dataSourceOptions) => {
230
+ const { collections, ...dataSource } = dataSourceOptions;
231
+ this.upsertDataSource(dataSource);
232
+ if (collections) {
233
+ this.getDataSource(dataSource.key)?.setCollections(collections, { clearFields: true });
234
+ }
235
+ });
236
+ return;
237
+ }
238
+
239
+ const dataSource = this.getDataSource(key);
240
+ if (!dataSource) {
241
+ return;
242
+ }
243
+ dataSource.setCollections(result?.collections || [], { clearFields: true });
244
+ }
245
+
246
+ protected async loadKey(key: string, options: { initial: boolean; force: boolean }) {
247
+ const loader = this.loaders.get(key);
248
+ if (!loader) {
249
+ return;
250
+ }
251
+
252
+ if (!this.getDataSource(key) && key !== '*') {
253
+ this.addDataSource({ key });
254
+ }
255
+
256
+ const { initial } = options;
257
+ this.loadingKeys.add(key);
258
+ if (key !== '*') {
259
+ this.setDataSourceState(key, {
260
+ status: initial ? 'loading' : 'reloading',
261
+ errorMessage: undefined,
262
+ });
263
+ }
264
+ this.loadErrors.set(key, null);
265
+
266
+ try {
267
+ const result = (await loader({ key, manager: this })) || {};
268
+ this.applyDataSourceLoadResult(key, result);
269
+ this.loadedKeys.add(key);
270
+ if (key !== '*') {
271
+ this.setDataSourceState(key, {
272
+ status: 'loaded',
273
+ errorMessage: undefined,
274
+ });
275
+ }
276
+ this.dispatchDataSourceEvent('dataSource:loaded', { dataSourceKey: key, initial });
277
+ } catch (error) {
278
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
279
+ this.loadErrors.set(key, normalizedError);
280
+ if (key !== '*') {
281
+ this.setDataSourceState(key, {
282
+ status: initial ? 'loading-failed' : 'reloading-failed',
283
+ errorMessage: normalizedError.message,
284
+ });
285
+ }
286
+ this.dispatchDataSourceEvent('dataSource:loadFailed', {
287
+ dataSourceKey: key,
288
+ initial,
289
+ error: normalizedError,
290
+ });
291
+ throw normalizedError;
292
+ } finally {
293
+ this.loadingKeys.delete(key);
294
+ }
295
+ }
85
296
  }
86
297
 
87
298
  export class DataSource {
@@ -110,6 +321,14 @@ export class DataSource {
110
321
  return this.options.key;
111
322
  }
112
323
 
324
+ get status() {
325
+ return this.options.status;
326
+ }
327
+
328
+ get errorMessage() {
329
+ return this.options.errorMessage;
330
+ }
331
+
113
332
  setDataSourceManager(dataSourceManager: DataSourceManager) {
114
333
  this.dataSourceManager = dataSourceManager;
115
334
  }
@@ -149,6 +368,10 @@ export class DataSource {
149
368
  return this.collectionManager.upsertCollections(collections, options);
150
369
  }
151
370
 
371
+ setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) {
372
+ return this.collectionManager.setCollections(collections, options);
373
+ }
374
+
152
375
  removeCollection(name: string) {
153
376
  return this.collectionManager.removeCollection(name);
154
377
  }
@@ -162,6 +385,14 @@ export class DataSource {
162
385
  Object.assign(this.options, newOptions);
163
386
  }
164
387
 
388
+ patchOptions(newOptions: any = {}) {
389
+ Object.assign(this.options, newOptions);
390
+ }
391
+
392
+ reload() {
393
+ return this.dataSourceManager.reloadDataSource(this.key);
394
+ }
395
+
165
396
  getCollectionField(fieldPath: string) {
166
397
  const [collectionName, ...otherKeys] = fieldPath.split('.');
167
398
  const fieldName = otherKeys.join('.');
@@ -194,6 +425,11 @@ export class CollectionManager {
194
425
  this.collections = observable.shallow<Map<string, Collection>>(new Map());
195
426
  }
196
427
 
428
+ protected resetCaches() {
429
+ this.childrenCollectionsName = {};
430
+ this.allCollectionsInheritChain = undefined;
431
+ }
432
+
197
433
  get flowEngine() {
198
434
  return this.dataSource.flowEngine;
199
435
  }
@@ -208,10 +444,12 @@ export class CollectionManager {
208
444
  col.setDataSource(this.dataSource);
209
445
  col.initInherits();
210
446
  this.collections.set(col.name, col);
447
+ this.resetCaches();
211
448
  }
212
449
 
213
450
  removeCollection(name: string) {
214
451
  this.collections.delete(name);
452
+ this.resetCaches();
215
453
  }
216
454
 
217
455
  updateCollection(newOptions: CollectionOptions, options: { clearFields?: boolean } = {}) {
@@ -220,6 +458,7 @@ export class CollectionManager {
220
458
  throw new Error(`Collection ${newOptions.name} not found`);
221
459
  }
222
460
  collection.setOptions(newOptions, options);
461
+ this.resetCaches();
223
462
  }
224
463
 
225
464
  upsertCollection(options: CollectionOptions) {
@@ -239,6 +478,12 @@ export class CollectionManager {
239
478
  this.addCollection(collection);
240
479
  }
241
480
  }
481
+ this.resetCaches();
482
+ }
483
+
484
+ setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) {
485
+ this.clearCollections();
486
+ this.upsertCollections(collections, options);
242
487
  }
243
488
 
244
489
  sortCollectionsByInherits(collections: CollectionOptions[]): CollectionOptions[] {
@@ -315,6 +560,7 @@ export class CollectionManager {
315
560
 
316
561
  clearCollections() {
317
562
  this.collections.clear();
563
+ this.resetCaches();
318
564
  }
319
565
 
320
566
  getAssociation(associationName: string): CollectionField | undefined {
@@ -537,6 +783,10 @@ export class Collection {
537
783
  this.upsertFields(this.options.fields || []);
538
784
  }
539
785
 
786
+ setOption(key: string, value: any) {
787
+ this.options[key] = value;
788
+ }
789
+
540
790
  getFields(): CollectionField[] {
541
791
  // 合并自身 fields 和所有 inherits 的 fields,后者优先被覆盖
542
792
  const fieldMap = new Map<string, CollectionField>();
@@ -3006,8 +3006,10 @@ export class FlowContext {
3006
3006
  }
3007
3007
 
3008
3008
  class BaseFlowEngineContext extends FlowContext {
3009
+ declare t: (key: any, options?: any) => string;
3009
3010
  declare router: Router;
3010
3011
  declare dataSourceManager: DataSourceManager;
3012
+ declare isDarkTheme: boolean;
3011
3013
  declare requireAsync: (url: string) => Promise<any>;
3012
3014
  declare importAsync: (url: string) => Promise<any>;
3013
3015
  declare createJSRunner: (options?: JSRunnerOptions) => Promise<JSRunner>;
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
- return this.context.i18n.t(key, options);
55
+ const translated = this.context.i18n.t(key, options);
56
+ return translated == null || translated === '' ? key : translated;
56
57
  }
57
58
  // 如果没有翻译函数,返回原始键值
58
59
  return key;
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure } from '@nocobase/flow-engine';
10
+ import type { DefaultStructure } from '../types';
11
11
  import { CollectionFieldModel } from './CollectionFieldModel';
12
12
 
13
13
  export class DisplayItemModel<T extends DefaultStructure = DefaultStructure> extends CollectionFieldModel<T> {}
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure } from '@nocobase/flow-engine';
10
+ import type { DefaultStructure } from '../types';
11
11
  import { CollectionFieldModel } from './CollectionFieldModel';
12
12
 
13
13
  export class EditableItemModel<T extends DefaultStructure = DefaultStructure> extends CollectionFieldModel<T> {}
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure } from '@nocobase/flow-engine';
10
+ import type { DefaultStructure } from '../types';
11
11
  import { CollectionFieldModel } from './CollectionFieldModel';
12
12
 
13
13
  export class FilterableItemModel<T extends DefaultStructure = DefaultStructure> extends CollectionFieldModel<T> {}
@@ -60,16 +60,22 @@ const modelMetas = new WeakMap<typeof FlowModel, FlowModelMeta>();
60
60
  const modelGlobalRegistries = new WeakMap<typeof FlowModel, GlobalFlowRegistry>();
61
61
 
62
62
  type BaseMenuItem = NonNullable<MenuProps['items']>[number];
63
- type MenuLeafItem = Exclude<BaseMenuItem, { children: MenuProps['items'] }>;
63
+ type MenuBaseItem = Omit<Exclude<BaseMenuItem, null>, 'key' | 'children'>;
64
64
 
65
- export type FlowModelExtraMenuItem = Omit<MenuLeafItem, 'key'> & {
65
+ export type FlowModelExtraMenuItem = MenuBaseItem & {
66
66
  key: React.Key;
67
67
  group?: string;
68
68
  sort?: number;
69
+ label?: React.ReactNode;
70
+ disabled?: boolean;
69
71
  onClick?: () => void;
72
+ children?: FlowModelExtraMenuItem[];
70
73
  };
71
74
 
72
- type FlowModelExtraMenuItemInput = Omit<FlowModelExtraMenuItem, 'key'> & { key?: React.Key };
75
+ type FlowModelExtraMenuItemInput = Omit<FlowModelExtraMenuItem, 'key' | 'children'> & {
76
+ key?: React.Key;
77
+ children?: FlowModelExtraMenuItemInput[];
78
+ };
73
79
 
74
80
  type ExtraMenuItemEntry = {
75
81
  group?: string;
@@ -86,6 +92,56 @@ type ExtraMenuItemEntry = {
86
92
 
87
93
  const classMenuExtensions = new WeakMap<typeof FlowModel, Set<ExtraMenuItemEntry>>();
88
94
 
95
+ const sortExtraMenuItems = (items: FlowModelExtraMenuItem[]) => {
96
+ return [...items].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
97
+ };
98
+
99
+ const isFlowModelExtraMenuItem = (item: FlowModelExtraMenuItem | null): item is FlowModelExtraMenuItem => {
100
+ return item !== null;
101
+ };
102
+
103
+ const normalizeExtraMenuItem = (
104
+ item: FlowModelExtraMenuItemInput,
105
+ {
106
+ group,
107
+ sort,
108
+ prefix,
109
+ path,
110
+ }: {
111
+ group: string;
112
+ sort: number;
113
+ prefix: string;
114
+ path: string;
115
+ },
116
+ ): FlowModelExtraMenuItem | null => {
117
+ if (!item) {
118
+ return null;
119
+ }
120
+
121
+ const normalizedGroup = item.group || group;
122
+ const normalizedSort = typeof item.sort === 'number' ? item.sort : sort;
123
+ const normalizedChildren = sortExtraMenuItems(
124
+ (item.children || [])
125
+ .map((child, index) =>
126
+ normalizeExtraMenuItem(child, {
127
+ group: normalizedGroup,
128
+ sort: normalizedSort,
129
+ prefix,
130
+ path: `${path}-${index}`,
131
+ }),
132
+ )
133
+ .filter(isFlowModelExtraMenuItem),
134
+ );
135
+
136
+ return {
137
+ ...item,
138
+ key: item.key ?? `${prefix}-${normalizedGroup}-${path}`,
139
+ group: normalizedGroup,
140
+ sort: normalizedSort,
141
+ children: normalizedChildren.length ? normalizedChildren : undefined,
142
+ };
143
+ };
144
+
89
145
  async function loadOpenStepSettingsDialog() {
90
146
  const mod = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
91
147
  return mod.openStepSettingsDialog;
@@ -1194,17 +1250,17 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1194
1250
  return model;
1195
1251
  }
1196
1252
 
1197
- filterSubModels<K extends keyof Structure['subModels'], R>(
1253
+ filterSubModels<K extends keyof NonNullable<Structure['subModels']>, R>(
1198
1254
  subKey: K,
1199
- callback: (model: ArrayElementType<Structure['subModels'][K]>, index: number) => boolean,
1200
- ): ArrayElementType<Structure['subModels'][K]>[] {
1255
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>, index: number) => boolean,
1256
+ ): ArrayElementType<NonNullable<Structure['subModels']>[K]>[] {
1201
1257
  const model = (this.subModels as any)[subKey as string];
1202
1258
 
1203
1259
  if (!model) {
1204
1260
  return [];
1205
1261
  }
1206
1262
 
1207
- const results: ArrayElementType<Structure['subModels'][K]>[] = [];
1263
+ const results: ArrayElementType<NonNullable<Structure['subModels']>[K]>[] = [];
1208
1264
 
1209
1265
  _.castArray(model)
1210
1266
  .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
@@ -1218,9 +1274,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1218
1274
  return results;
1219
1275
  }
1220
1276
 
1221
- mapSubModels<K extends keyof Structure['subModels'], R>(
1277
+ mapSubModels<K extends keyof NonNullable<Structure['subModels']>, R>(
1222
1278
  subKey: K,
1223
- callback: (model: ArrayElementType<Structure['subModels'][K]>, index: number) => R,
1279
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>, index: number) => R,
1224
1280
  ): R[] {
1225
1281
  const model = (this.subModels as any)[subKey as string];
1226
1282
 
@@ -1240,7 +1296,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1240
1296
  return results;
1241
1297
  }
1242
1298
 
1243
- hasSubModel<K extends keyof Structure['subModels']>(subKey: K) {
1299
+ hasSubModel<K extends keyof NonNullable<Structure['subModels']>>(subKey: K) {
1244
1300
  const subModel = (this.subModels as any)[subKey as string];
1245
1301
  if (!subModel) {
1246
1302
  return false;
@@ -1248,10 +1304,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1248
1304
  return _.castArray(subModel).length > 0;
1249
1305
  }
1250
1306
 
1251
- findSubModel<K extends keyof Structure['subModels'], R>(
1307
+ findSubModel<K extends keyof NonNullable<Structure['subModels']>, R>(
1252
1308
  subKey: K,
1253
- callback: (model: ArrayElementType<Structure['subModels'][K]>) => R,
1254
- ): ArrayElementType<Structure['subModels'][K]> | null {
1309
+ callback: (model: ArrayElementType<NonNullable<Structure['subModels']>[K]>) => R,
1310
+ ): ArrayElementType<NonNullable<Structure['subModels']>[K]> | null {
1255
1311
  const model = (this.subModels as any)[subKey as string];
1256
1312
 
1257
1313
  if (!model) {
@@ -1261,7 +1317,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1261
1317
  return (
1262
1318
  (_.castArray(model).find((item) => {
1263
1319
  return (callback as (model: any) => R)(item);
1264
- }) as ArrayElementType<Structure['subModels'][K]> | undefined) || null
1320
+ }) as ArrayElementType<NonNullable<Structure['subModels']>[K]> | undefined) || null
1265
1321
  );
1266
1322
  }
1267
1323
 
@@ -1607,6 +1663,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1607
1663
  seen.add(Cls);
1608
1664
  const reg = classMenuExtensions.get(Cls);
1609
1665
  if (reg) {
1666
+ let entryIndex = 0;
1610
1667
  for (const entry of reg) {
1611
1668
  if (entry.matcher && !entry.matcher(model)) continue;
1612
1669
  const items =
@@ -1614,16 +1671,21 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1614
1671
  const group = entry.group || 'common-actions';
1615
1672
  const sort = entry.sort ?? 0;
1616
1673
  const prefix = entry.keyPrefix || Cls.name || 'extra';
1617
- (items || []).forEach((it, idx: number) => {
1618
- if (!it) return;
1619
- const key = it.key ?? `${prefix}-${group}-${idx}-${Math.random().toString(36).slice(2, 6)}`;
1620
- collected.push({
1621
- ...it,
1622
- key,
1623
- group: it.group || group,
1624
- sort: typeof it.sort === 'number' ? it.sort : sort,
1625
- });
1674
+ sortExtraMenuItems(
1675
+ (items || [])
1676
+ .map((it, idx: number) =>
1677
+ normalizeExtraMenuItem(it, {
1678
+ group,
1679
+ sort,
1680
+ prefix,
1681
+ path: `${entryIndex}-${idx}`,
1682
+ }),
1683
+ )
1684
+ .filter(isFlowModelExtraMenuItem),
1685
+ ).forEach((it) => {
1686
+ collected.push(it);
1626
1687
  });
1688
+ entryIndex += 1;
1627
1689
  }
1628
1690
  }
1629
1691
  const ParentClass = Object.getPrototypeOf(Cls) as typeof FlowModel;
package/src/provider.tsx CHANGED
@@ -45,34 +45,50 @@ export const FlowEngineGlobalsContextProvider: React.FC<{ children: React.ReactN
45
45
  const engine = useFlowEngine();
46
46
  const config = useContext(ConfigProvider.ConfigContext);
47
47
  const { token } = theme.useToken();
48
+ const isDarkTheme = React.useMemo(() => {
49
+ const algorithm = config?.theme?.algorithm;
50
+ if (Array.isArray(algorithm)) {
51
+ return algorithm.includes(theme.darkAlgorithm);
52
+ }
53
+ return algorithm === theme.darkAlgorithm;
54
+ }, [config]);
48
55
 
49
- useEffect(() => {
50
- const context = {
51
- antdConfig: config,
52
- // themeToken 改为可观察的 getter,在下方单独 define
53
- modal,
54
- message,
55
- notification,
56
- };
57
- engine.context.defineProperty('viewer', {
58
- cache: false,
59
- get: (ctx) => new FlowViewer(ctx, { drawer, embed, popover, dialog }),
60
- });
61
- for (const item of Object.entries(context)) {
62
- const [key, value] = item;
63
- if (value) {
64
- engine.context.defineProperty(key, { value });
65
- }
56
+ // 这些全局能力需要在 children 首次渲染前就可读,不能等到 effect 后再挂到上下文。
57
+ engine.context.defineProperty('viewer', {
58
+ cache: false,
59
+ get: (ctx) => new FlowViewer(ctx, { drawer, embed, popover, dialog }),
60
+ });
61
+ for (const item of Object.entries({
62
+ antdConfig: config,
63
+ modal,
64
+ message,
65
+ notification,
66
+ })) {
67
+ const [key, value] = item;
68
+ if (value) {
69
+ engine.context.defineProperty(key, { value });
66
70
  }
67
- // 将 themeToken 定义为 observable, 使组件能够响应主题的变更
68
- // NOTE: 必须在 antdConfig 写入后再更新 themeToken;否则会读取到旧 antdConfig 的值。
69
- engine.context.defineProperty('themeToken', {
70
- get: () => token,
71
- observable: true,
72
- cache: true,
73
- });
71
+ }
72
+ // themeToken 定义为 observable, 使组件能够响应主题的变更。
73
+ engine.context.defineProperty('themeToken', {
74
+ get: () => token,
75
+ observable: true,
76
+ cache: true,
77
+ });
78
+ // 统一把暗色模式暴露到 Flow 上下文,避免 flow 侧继续依赖 global-theme。
79
+ engine.context.defineProperty('isDarkTheme', {
80
+ get: () => isDarkTheme,
81
+ observable: true,
82
+ cache: true,
83
+ info: {
84
+ description: 'Whether current theme algorithm is dark mode.',
85
+ detail: 'boolean',
86
+ },
87
+ });
88
+
89
+ useEffect(() => {
74
90
  engine.reactView.refresh();
75
- }, [engine, drawer, modal, message, notification, config, popover, token, dialog, embed]);
91
+ }, [engine, drawer, modal, message, notification, config, popover, token, dialog, embed, isDarkTheme]);
76
92
 
77
93
  return (
78
94
  <ConfigProvider {...config} locale={engine.context.locales?.antd} popupMatchSelectWidth={false}>