@nocobase/flow-engine 2.1.0-beta.8 → 2.2.0-alpha.1

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 (215) hide show
  1. package/lib/FlowContextProvider.d.ts +5 -1
  2. package/lib/FlowContextProvider.js +9 -2
  3. package/lib/components/FieldModelRenderer.js +2 -2
  4. package/lib/components/FlowModelRenderer.d.ts +3 -1
  5. package/lib/components/FlowModelRenderer.js +12 -6
  6. package/lib/components/FormItem.d.ts +6 -0
  7. package/lib/components/FormItem.js +11 -3
  8. package/lib/components/MobilePopup.js +6 -5
  9. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  10. package/lib/components/dnd/gridDragPlanner.js +607 -19
  11. package/lib/components/dnd/index.d.ts +31 -2
  12. package/lib/components/dnd/index.js +244 -23
  13. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  14. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  15. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  16. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
  17. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  18. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  19. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  20. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  21. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  22. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  23. package/lib/components/subModel/AddSubModelButton.js +12 -1
  24. package/lib/components/subModel/LazyDropdown.js +301 -52
  25. package/lib/components/subModel/index.d.ts +1 -0
  26. package/lib/components/subModel/index.js +19 -0
  27. package/lib/components/subModel/utils.d.ts +2 -1
  28. package/lib/components/subModel/utils.js +15 -5
  29. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  30. package/lib/components/variables/VariableHybridInput.js +499 -0
  31. package/lib/components/variables/index.d.ts +2 -0
  32. package/lib/components/variables/index.js +3 -0
  33. package/lib/data-source/index.d.ts +84 -0
  34. package/lib/data-source/index.js +269 -7
  35. package/lib/executor/FlowExecutor.js +6 -3
  36. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  37. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  38. package/lib/flow-registry/index.d.ts +1 -0
  39. package/lib/flow-registry/index.js +3 -1
  40. package/lib/flowContext.d.ts +9 -1
  41. package/lib/flowContext.js +77 -6
  42. package/lib/flowEngine.d.ts +136 -4
  43. package/lib/flowEngine.js +429 -51
  44. package/lib/flowI18n.js +2 -1
  45. package/lib/flowSettings.d.ts +14 -6
  46. package/lib/flowSettings.js +34 -6
  47. package/lib/index.d.ts +2 -0
  48. package/lib/index.js +7 -0
  49. package/lib/lazy-helper.d.ts +14 -0
  50. package/lib/lazy-helper.js +71 -0
  51. package/lib/locale/en-US.json +1 -0
  52. package/lib/locale/index.d.ts +2 -0
  53. package/lib/locale/zh-CN.json +1 -0
  54. package/lib/models/DisplayItemModel.d.ts +1 -1
  55. package/lib/models/EditableItemModel.d.ts +1 -1
  56. package/lib/models/FilterableItemModel.d.ts +1 -1
  57. package/lib/models/flowModel.d.ts +13 -10
  58. package/lib/models/flowModel.js +126 -34
  59. package/lib/provider.js +38 -23
  60. package/lib/reactive/observer.js +46 -16
  61. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
  62. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
  63. package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
  64. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
  65. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
  66. package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
  67. package/lib/runjs-context/contexts/base.js +464 -29
  68. package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
  69. package/lib/runjs-context/contexts/elementDoc.js +152 -0
  70. package/lib/runjs-context/setup.js +1 -0
  71. package/lib/runjs-context/snippets/index.js +13 -2
  72. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  74. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  75. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  76. package/lib/types.d.ts +50 -2
  77. package/lib/types.js +1 -0
  78. package/lib/utils/createCollectionContextMeta.js +6 -2
  79. package/lib/utils/index.d.ts +3 -2
  80. package/lib/utils/index.js +7 -0
  81. package/lib/utils/loadedPageCache.d.ts +24 -0
  82. package/lib/utils/loadedPageCache.js +139 -0
  83. package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
  84. package/lib/utils/parsePathnameToViewParams.js +28 -4
  85. package/lib/utils/randomId.d.ts +39 -0
  86. package/lib/utils/randomId.js +45 -0
  87. package/lib/utils/runjsTemplateCompat.js +1 -1
  88. package/lib/utils/runjsValue.js +41 -11
  89. package/lib/utils/schema-utils.d.ts +7 -1
  90. package/lib/utils/schema-utils.js +19 -0
  91. package/lib/views/FlowView.d.ts +7 -1
  92. package/lib/views/FlowView.js +11 -1
  93. package/lib/views/PageComponent.js +8 -6
  94. package/lib/views/ViewNavigation.d.ts +12 -2
  95. package/lib/views/ViewNavigation.js +28 -9
  96. package/lib/views/createViewMeta.js +114 -50
  97. package/lib/views/inheritLayoutContext.d.ts +10 -0
  98. package/lib/views/inheritLayoutContext.js +50 -0
  99. package/lib/views/runViewBeforeClose.d.ts +10 -0
  100. package/lib/views/runViewBeforeClose.js +45 -0
  101. package/lib/views/useDialog.d.ts +2 -1
  102. package/lib/views/useDialog.js +12 -3
  103. package/lib/views/useDrawer.d.ts +2 -1
  104. package/lib/views/useDrawer.js +12 -3
  105. package/lib/views/usePage.d.ts +5 -11
  106. package/lib/views/usePage.js +304 -144
  107. package/package.json +5 -4
  108. package/src/FlowContextProvider.tsx +9 -1
  109. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  110. package/src/__tests__/flow-engine.test.ts +166 -0
  111. package/src/__tests__/flowContext.test.ts +105 -1
  112. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  113. package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
  114. package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
  115. package/src/__tests__/flowSettings.test.ts +94 -15
  116. package/src/__tests__/objectVariable.test.ts +24 -0
  117. package/src/__tests__/provider.test.tsx +24 -2
  118. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  119. package/src/__tests__/runjsContext.test.ts +21 -0
  120. package/src/__tests__/runjsContextImplementations.test.ts +9 -2
  121. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  122. package/src/__tests__/runjsLocales.test.ts +6 -5
  123. package/src/__tests__/runjsSnippets.test.ts +21 -0
  124. package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
  125. package/src/components/FieldModelRenderer.tsx +2 -1
  126. package/src/components/FlowModelRenderer.tsx +18 -6
  127. package/src/components/FormItem.tsx +7 -1
  128. package/src/components/MobilePopup.tsx +4 -2
  129. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  130. package/src/components/__tests__/FormItem.test.tsx +25 -0
  131. package/src/components/__tests__/dnd.test.ts +44 -0
  132. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  133. package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
  134. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  135. package/src/components/dnd/gridDragPlanner.ts +750 -17
  136. package/src/components/dnd/index.tsx +305 -28
  137. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  138. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
  139. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
  141. package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
  142. package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
  143. package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
  144. package/src/components/subModel/AddSubModelButton.tsx +16 -2
  145. package/src/components/subModel/LazyDropdown.tsx +341 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
  147. package/src/components/subModel/__tests__/utils.test.ts +24 -0
  148. package/src/components/subModel/index.ts +1 -0
  149. package/src/components/subModel/utils.ts +13 -2
  150. package/src/components/variables/VariableHybridInput.tsx +531 -0
  151. package/src/components/variables/index.ts +2 -0
  152. package/src/data-source/__tests__/collection.test.ts +41 -2
  153. package/src/data-source/__tests__/index.test.ts +69 -2
  154. package/src/data-source/index.ts +332 -8
  155. package/src/executor/FlowExecutor.ts +6 -3
  156. package/src/executor/__tests__/flowExecutor.test.ts +57 -0
  157. package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
  158. package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
  159. package/src/flow-registry/index.ts +1 -0
  160. package/src/flowContext.ts +85 -6
  161. package/src/flowEngine.ts +484 -45
  162. package/src/flowI18n.ts +2 -1
  163. package/src/flowSettings.ts +40 -6
  164. package/src/index.ts +2 -0
  165. package/src/lazy-helper.tsx +57 -0
  166. package/src/locale/en-US.json +1 -0
  167. package/src/locale/zh-CN.json +1 -0
  168. package/src/models/DisplayItemModel.tsx +1 -1
  169. package/src/models/EditableItemModel.tsx +1 -1
  170. package/src/models/FilterableItemModel.tsx +1 -1
  171. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  172. package/src/models/__tests__/flowModel.test.ts +65 -37
  173. package/src/models/flowModel.tsx +184 -65
  174. package/src/provider.tsx +41 -25
  175. package/src/reactive/__tests__/observer.test.tsx +82 -0
  176. package/src/reactive/observer.tsx +87 -25
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
  179. package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
  180. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
  181. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
  182. package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
  183. package/src/runjs-context/contexts/base.ts +467 -31
  184. package/src/runjs-context/contexts/elementDoc.ts +130 -0
  185. package/src/runjs-context/setup.ts +1 -0
  186. package/src/runjs-context/snippets/index.ts +12 -1
  187. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  188. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  189. package/src/types.ts +62 -0
  190. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  191. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
  192. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  193. package/src/utils/__tests__/utils.test.ts +62 -0
  194. package/src/utils/createCollectionContextMeta.ts +6 -2
  195. package/src/utils/index.ts +5 -1
  196. package/src/utils/loadedPageCache.ts +147 -0
  197. package/src/utils/parsePathnameToViewParams.ts +45 -5
  198. package/src/utils/randomId.ts +48 -0
  199. package/src/utils/runjsTemplateCompat.ts +1 -1
  200. package/src/utils/runjsValue.ts +50 -11
  201. package/src/utils/schema-utils.ts +30 -1
  202. package/src/views/FlowView.tsx +22 -2
  203. package/src/views/PageComponent.tsx +7 -4
  204. package/src/views/ViewNavigation.ts +46 -9
  205. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  206. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  207. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  208. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  209. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
  210. package/src/views/createViewMeta.ts +106 -34
  211. package/src/views/inheritLayoutContext.ts +26 -0
  212. package/src/views/runViewBeforeClose.ts +19 -0
  213. package/src/views/useDialog.tsx +13 -3
  214. package/src/views/useDrawer.tsx +13 -3
  215. package/src/views/usePage.tsx +367 -180
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, expect, it } from 'vitest';
10
+ import { describe, expect, it, vi } from 'vitest';
11
11
  import { DataSource, DataSourceManager } from '../index';
12
12
  import { FlowEngine } from '../../flowEngine';
13
13
 
@@ -50,7 +50,7 @@ describe('DataSource & Collection APIs', () => {
50
50
  { name: 'body', type: 'string', interface: 'text' },
51
51
  ],
52
52
  });
53
- const bodyField = ds.getCollection('posts')!.getField('body');
53
+ const bodyField = ds.getCollection('posts')?.getField('body');
54
54
  expect(bodyField?.name).toBe('body');
55
55
 
56
56
  // remove collection
@@ -79,4 +79,71 @@ describe('DataSource & Collection APIs', () => {
79
79
  ]),
80
80
  ).toThrow(/circular/);
81
81
  });
82
+
83
+ it('translates validation messages from data-source-main in component rules', async () => {
84
+ const { m, engine } = makeManager();
85
+ engine.context.i18n = {
86
+ t: (key: string, options?: Record<string, any>) => {
87
+ if (key === 'string.length' && options?.ns === 'data-source-main') {
88
+ return `${options.label} 长度必须为 ${options.limit} 个字符`;
89
+ }
90
+ return key;
91
+ },
92
+ } as any;
93
+
94
+ const ds = new DataSource({ key: 'main' });
95
+ m.addDataSource(ds);
96
+ ds.addCollection({
97
+ name: 'posts',
98
+ fields: [
99
+ {
100
+ name: 'title',
101
+ type: 'string',
102
+ interface: 'text',
103
+ title: '单行文本',
104
+ validation: {
105
+ type: 'string',
106
+ rules: [{ name: 'length', args: { limit: 18 } }],
107
+ },
108
+ },
109
+ ],
110
+ });
111
+
112
+ const rules = ds.getCollection('posts')?.getField('title')?.getComponentProps().rules || [];
113
+
114
+ await expect(rules[0].validator({}, '123')).rejects.toBe('单行文本 长度必须为 18 个字符');
115
+ });
116
+
117
+ it('ensureLoaded, reload and data source events work for main loader', async () => {
118
+ const { m, engine } = makeManager();
119
+ const loadedListener = vi.fn();
120
+ const failedListener = vi.fn();
121
+ engine.context.app = { eventBus: new EventTarget() } as any;
122
+ engine.context.app.eventBus.addEventListener('dataSource:loaded', loadedListener);
123
+ engine.context.app.eventBus.addEventListener('dataSource:loadFailed', failedListener);
124
+
125
+ const loader = vi
126
+ .fn()
127
+ .mockResolvedValueOnce({
128
+ collections: [{ name: 'posts', fields: [{ name: 'title', type: 'string', interface: 'input' }] }],
129
+ })
130
+ .mockResolvedValueOnce({
131
+ collections: [{ name: 'users', fields: [{ name: 'nickname', type: 'string', interface: 'input' }] }],
132
+ });
133
+
134
+ m.registerLoader('main', loader);
135
+
136
+ await m.ensureLoaded();
137
+ expect(loader).toHaveBeenCalledTimes(1);
138
+ expect(m.getDataSource('main')?.status).toBe('loaded');
139
+ expect(m.getCollection('main', 'posts')?.name).toBe('posts');
140
+ expect(loadedListener).toHaveBeenCalledTimes(1);
141
+
142
+ await m.reloadDataSource('main');
143
+ expect(loader).toHaveBeenCalledTimes(2);
144
+ expect(m.getCollection('main', 'posts')).toBeUndefined();
145
+ expect(m.getCollection('main', 'users')?.name).toBe('users');
146
+ expect(m.getDataSource('main')?.reload).toBeTypeOf('function');
147
+ expect(failedListener).not.toHaveBeenCalled();
148
+ });
82
149
  });
@@ -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,37 @@ 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
+ registerFieldFilterOperator?: (operator: any) => void;
41
+ registerFieldFilterOperatorGroup?: (name: string, operators?: any[]) => void;
42
+ addFieldFilterOperatorsToGroup?: (name: string, operators?: any[]) => void;
43
+ getFieldInterface?: (name: string) => any;
44
+ registerFieldInterfaceConfigure?: (options: unknown) => void;
45
+ getFieldInterfaceConfigure?: (name: string, collectionInfo?: unknown) => unknown;
46
+ getFieldInterfaceConfigureProperties?: (name: string, collectionInfo?: any) => Record<string, any>;
47
+ };
48
+ loaders = new Map<string, DataSourceLoader>();
49
+ loadedKeys = new Set<string>();
50
+ loadingKeys = new Set<string>();
51
+ loadErrors = new Map<string, Error | null>();
52
+ loadingPromise: Promise<void> | null = null;
26
53
 
27
54
  constructor() {
28
55
  this.dataSources = observable.shallow<Map<string, DataSource>>(new Map());
@@ -32,6 +59,50 @@ export class DataSourceManager {
32
59
  this.flowEngine = flowEngine;
33
60
  }
34
61
 
62
+ setRequester(requester?: DataSourceRequester) {
63
+ this.requester = requester;
64
+ }
65
+
66
+ setCollectionFieldInterfaceManager(manager: DataSourceManager['collectionFieldInterfaceManager']) {
67
+ this.collectionFieldInterfaceManager = manager;
68
+ }
69
+
70
+ addFieldInterfaces(fieldInterfaceClasses: any[] = []) {
71
+ this.collectionFieldInterfaceManager?.addFieldInterfaces?.(fieldInterfaceClasses);
72
+ }
73
+
74
+ addFieldInterfaceGroups(groups: Record<string, { label: string; order?: number }>) {
75
+ this.collectionFieldInterfaceManager?.addFieldInterfaceGroups?.(groups);
76
+ }
77
+
78
+ addFieldInterfaceComponentOption(name: string, option: any) {
79
+ this.collectionFieldInterfaceManager?.addFieldInterfaceComponentOption?.(name, option);
80
+ }
81
+
82
+ addFieldInterfaceOperator(name: string, operator: any) {
83
+ this.collectionFieldInterfaceManager?.addFieldInterfaceOperator?.(name, operator);
84
+ }
85
+
86
+ registerFieldFilterOperator(operator: any) {
87
+ this.collectionFieldInterfaceManager?.registerFieldFilterOperator?.(operator);
88
+ }
89
+
90
+ registerFieldFilterOperatorGroup(name: string, operators: any[] = []) {
91
+ this.collectionFieldInterfaceManager?.registerFieldFilterOperatorGroup?.(name, operators);
92
+ }
93
+
94
+ addFieldFilterOperatorsToGroup(name: string, operators: any[] = []) {
95
+ this.collectionFieldInterfaceManager?.addFieldFilterOperatorsToGroup?.(name, operators);
96
+ }
97
+
98
+ registerLoader(key: string, loader: DataSourceLoader) {
99
+ this.loaders.set(key, loader);
100
+ }
101
+
102
+ removeLoader(key: string) {
103
+ this.loaders.delete(key);
104
+ }
105
+
35
106
  addDataSource(ds: DataSource | DataSourceOptions) {
36
107
  if (this.dataSources.has(ds.key)) {
37
108
  throw new Error(`DataSource with name ${ds.key} already exists`);
@@ -48,7 +119,7 @@ export class DataSourceManager {
48
119
 
49
120
  upsertDataSource(ds: DataSource | DataSourceOptions) {
50
121
  if (this.dataSources.has(ds.key)) {
51
- this.dataSources.get(ds.key)?.setOptions(ds);
122
+ this.dataSources.get(ds.key)?.patchOptions(ds);
52
123
  } else {
53
124
  this.addDataSource(ds);
54
125
  }
@@ -82,6 +153,195 @@ export class DataSourceManager {
82
153
  if (!ds) return undefined;
83
154
  return ds.getCollectionField(otherKeys.join('.'));
84
155
  }
156
+
157
+ async ensureLoaded(options: { force?: boolean; keys?: string[] } = {}) {
158
+ const { force = false } = options;
159
+ const keys = this.resolveLoadKeys(options.keys);
160
+ const pendingKeys = force ? keys : keys.filter((key) => !this.loadedKeys.has(key));
161
+ if (!pendingKeys.length) {
162
+ return;
163
+ }
164
+ if (this.loadingPromise) {
165
+ return this.loadingPromise;
166
+ }
167
+
168
+ this.loadingPromise = (async () => {
169
+ try {
170
+ for (const key of pendingKeys) {
171
+ await this.loadKey(key, { initial: !this.loadedKeys.has(key), force });
172
+ }
173
+ } finally {
174
+ this.loadingPromise = null;
175
+ }
176
+ })();
177
+
178
+ return this.loadingPromise;
179
+ }
180
+
181
+ async reload(options: { keys?: string[] } = {}) {
182
+ const keys = this.resolveLoadKeys(options.keys);
183
+ if (!keys.length) {
184
+ return;
185
+ }
186
+ if (this.loadingPromise) {
187
+ return this.loadingPromise;
188
+ }
189
+
190
+ this.loadingPromise = (async () => {
191
+ try {
192
+ for (const key of keys) {
193
+ await this.loadKey(key, { initial: false, force: true });
194
+ }
195
+ } finally {
196
+ this.loadingPromise = null;
197
+ }
198
+ })();
199
+
200
+ return this.loadingPromise;
201
+ }
202
+
203
+ async reloadDataSource(key: string) {
204
+ if (this.loadingKeys.has(key) && this.loadingPromise) {
205
+ return this.loadingPromise;
206
+ }
207
+ if (!this.loaders.has(key) && this.loaders.has('*')) {
208
+ return this.reload({ keys: ['*'] });
209
+ }
210
+ return this.reload({ keys: [key] });
211
+ }
212
+
213
+ protected resolveLoadKeys(requestedKeys?: string[]) {
214
+ const normalizedKeys = requestedKeys?.length ? requestedKeys : ['main'];
215
+ const explicitKeys = normalizedKeys.filter((key) => this.loaders.has(key));
216
+ if (this.loaders.has('*')) {
217
+ return _.uniq(['*', ...explicitKeys]);
218
+ }
219
+ return explicitKeys.length ? explicitKeys : normalizedKeys;
220
+ }
221
+
222
+ protected getApp() {
223
+ return this.flowEngine?.context?.app as { eventBus?: EventTarget } | undefined;
224
+ }
225
+
226
+ protected dispatchDataSourceEvent(
227
+ type: 'dataSource:loaded' | 'dataSource:loadFailed',
228
+ detail: { dataSourceKey: string; initial: boolean; error?: Error },
229
+ ) {
230
+ this.getApp()?.eventBus?.dispatchEvent(new CustomEvent(type, { detail }));
231
+ }
232
+
233
+ protected setDataSourceState(
234
+ key: string,
235
+ options: Partial<Pick<DataSourceOptions, 'status' | 'errorMessage'>> & Record<string, any>,
236
+ ) {
237
+ const dataSource = this.getDataSource(key);
238
+ if (!dataSource) {
239
+ return;
240
+ }
241
+ dataSource.patchOptions(options);
242
+ }
243
+
244
+ protected applyDataSourceLoadResult(key: string, result: DataSourceLoadResult) {
245
+ if (key === '*') {
246
+ const dataSources = result?.dataSources || [];
247
+ dataSources.forEach((dataSourceOptions) => {
248
+ const { collections, ...dataSource } = dataSourceOptions;
249
+ this.upsertDataSource(dataSource);
250
+ if (collections) {
251
+ this.getDataSource(dataSource.key)?.setCollections(collections, { clearFields: true });
252
+ }
253
+ });
254
+ return;
255
+ }
256
+
257
+ const dataSource = this.getDataSource(key);
258
+ if (!dataSource) {
259
+ return;
260
+ }
261
+ dataSource.setCollections(result?.collections || [], { clearFields: true });
262
+ }
263
+
264
+ protected async loadKey(key: string, options: { initial: boolean; force: boolean }) {
265
+ const loader = this.loaders.get(key);
266
+ if (!loader) {
267
+ return;
268
+ }
269
+
270
+ if (!this.getDataSource(key) && key !== '*') {
271
+ this.addDataSource({ key });
272
+ }
273
+
274
+ const { initial } = options;
275
+ this.loadingKeys.add(key);
276
+ if (key !== '*') {
277
+ this.setDataSourceState(key, {
278
+ status: initial ? 'loading' : 'reloading',
279
+ errorMessage: undefined,
280
+ });
281
+ }
282
+ this.loadErrors.set(key, null);
283
+
284
+ try {
285
+ const result = (await loader({ key, manager: this })) || {};
286
+ this.applyDataSourceLoadResult(key, result);
287
+ this.loadedKeys.add(key);
288
+ if (key !== '*') {
289
+ this.setDataSourceState(key, {
290
+ status: 'loaded',
291
+ errorMessage: undefined,
292
+ });
293
+ }
294
+ this.dispatchDataSourceEvent('dataSource:loaded', { dataSourceKey: key, initial });
295
+ } catch (error) {
296
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
297
+ this.loadErrors.set(key, normalizedError);
298
+ if (key !== '*') {
299
+ this.setDataSourceState(key, {
300
+ status: initial ? 'loading-failed' : 'reloading-failed',
301
+ errorMessage: normalizedError.message,
302
+ });
303
+ }
304
+ this.dispatchDataSourceEvent('dataSource:loadFailed', {
305
+ dataSourceKey: key,
306
+ initial,
307
+ error: normalizedError,
308
+ });
309
+ throw normalizedError;
310
+ } finally {
311
+ this.loadingKeys.delete(key);
312
+ }
313
+ }
314
+ }
315
+
316
+ export type CollectionFieldInterfaceDataSourceManager = Pick<DataSourceManager, 'collectionFieldInterfaceManager'>;
317
+
318
+ export function getCollectionFieldInterface(
319
+ interfaceName: string | undefined,
320
+ ...dataSourceManagers: Array<CollectionFieldInterfaceDataSourceManager | null | undefined>
321
+ ) {
322
+ if (!interfaceName) {
323
+ return undefined;
324
+ }
325
+
326
+ // TODO: Once legacy client is removed and all runtimes share the client-v2 flow-engine
327
+ // DataSourceManager, callers should only pass the flow-engine context DataSourceManager.
328
+ for (const dataSourceManager of dataSourceManagers) {
329
+ const collectionFieldInterfaceManager = dataSourceManager?.collectionFieldInterfaceManager;
330
+ const getFieldInterface = collectionFieldInterfaceManager?.getFieldInterface;
331
+ if (typeof getFieldInterface === 'function') {
332
+ return getFieldInterface.call(collectionFieldInterfaceManager, interfaceName);
333
+ }
334
+ }
335
+
336
+ return undefined;
337
+ }
338
+
339
+ function shouldTranslateOptionLabel(label: unknown): label is string {
340
+ return typeof label === 'string' && /\{\{\s*t\s*\(/.test(label);
341
+ }
342
+
343
+ function translateOptionLabel(flowEngine: FlowEngine, label: unknown, options?: Record<string, any>) {
344
+ return shouldTranslateOptionLabel(label) ? flowEngine.translate(label, options) : label;
85
345
  }
86
346
 
87
347
  export class DataSource {
@@ -110,6 +370,14 @@ export class DataSource {
110
370
  return this.options.key;
111
371
  }
112
372
 
373
+ get status() {
374
+ return this.options.status;
375
+ }
376
+
377
+ get errorMessage() {
378
+ return this.options.errorMessage;
379
+ }
380
+
113
381
  setDataSourceManager(dataSourceManager: DataSourceManager) {
114
382
  this.dataSourceManager = dataSourceManager;
115
383
  }
@@ -149,6 +417,10 @@ export class DataSource {
149
417
  return this.collectionManager.upsertCollections(collections, options);
150
418
  }
151
419
 
420
+ setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) {
421
+ return this.collectionManager.setCollections(collections, options);
422
+ }
423
+
152
424
  removeCollection(name: string) {
153
425
  return this.collectionManager.removeCollection(name);
154
426
  }
@@ -162,6 +434,14 @@ export class DataSource {
162
434
  Object.assign(this.options, newOptions);
163
435
  }
164
436
 
437
+ patchOptions(newOptions: any = {}) {
438
+ Object.assign(this.options, newOptions);
439
+ }
440
+
441
+ reload() {
442
+ return this.dataSourceManager.reloadDataSource(this.key);
443
+ }
444
+
165
445
  getCollectionField(fieldPath: string) {
166
446
  const [collectionName, ...otherKeys] = fieldPath.split('.');
167
447
  const fieldName = otherKeys.join('.');
@@ -194,6 +474,11 @@ export class CollectionManager {
194
474
  this.collections = observable.shallow<Map<string, Collection>>(new Map());
195
475
  }
196
476
 
477
+ protected resetCaches() {
478
+ this.childrenCollectionsName = {};
479
+ this.allCollectionsInheritChain = undefined;
480
+ }
481
+
197
482
  get flowEngine() {
198
483
  return this.dataSource.flowEngine;
199
484
  }
@@ -208,10 +493,12 @@ export class CollectionManager {
208
493
  col.setDataSource(this.dataSource);
209
494
  col.initInherits();
210
495
  this.collections.set(col.name, col);
496
+ this.resetCaches();
211
497
  }
212
498
 
213
499
  removeCollection(name: string) {
214
500
  this.collections.delete(name);
501
+ this.resetCaches();
215
502
  }
216
503
 
217
504
  updateCollection(newOptions: CollectionOptions, options: { clearFields?: boolean } = {}) {
@@ -220,6 +507,7 @@ export class CollectionManager {
220
507
  throw new Error(`Collection ${newOptions.name} not found`);
221
508
  }
222
509
  collection.setOptions(newOptions, options);
510
+ this.resetCaches();
223
511
  }
224
512
 
225
513
  upsertCollection(options: CollectionOptions) {
@@ -239,6 +527,12 @@ export class CollectionManager {
239
527
  this.addCollection(collection);
240
528
  }
241
529
  }
530
+ this.resetCaches();
531
+ }
532
+
533
+ setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) {
534
+ this.clearCollections();
535
+ this.upsertCollections(collections, options);
242
536
  }
243
537
 
244
538
  sortCollectionsByInherits(collections: CollectionOptions[]): CollectionOptions[] {
@@ -315,6 +609,7 @@ export class CollectionManager {
315
609
 
316
610
  clearCollections() {
317
611
  this.collections.clear();
612
+ this.resetCaches();
318
613
  }
319
614
 
320
615
  getAssociation(associationName: string): CollectionField | undefined {
@@ -501,6 +796,12 @@ export class Collection {
501
796
 
502
797
  get titleCollectionField() {
503
798
  const titleFieldName = this.options.titleField || this.filterTargetKey;
799
+ if (Array.isArray(titleFieldName)) {
800
+ if (titleFieldName.length !== 1) {
801
+ return undefined;
802
+ }
803
+ return this.getField(titleFieldName[0]);
804
+ }
504
805
  const titleCollectionField = this.getField(titleFieldName);
505
806
  return titleCollectionField;
506
807
  }
@@ -531,6 +832,10 @@ export class Collection {
531
832
  this.upsertFields(this.options.fields || []);
532
833
  }
533
834
 
835
+ setOption(key: string, value: any) {
836
+ this.options[key] = value;
837
+ }
838
+
534
839
  getFields(): CollectionField[] {
535
840
  // 合并自身 fields 和所有 inherits 的 fields,后者优先被覆盖
536
841
  const fieldMap = new Map<string, CollectionField>();
@@ -789,7 +1094,7 @@ export class CollectionField {
789
1094
  }
790
1095
  return {
791
1096
  ...v,
792
- label: v.label ? this.flowEngine.translate(v.label, { ns: 'lm-collections' }) : v.label,
1097
+ label: translateOptionLabel(this.flowEngine, v.label, { ns: 'lm-collections' }),
793
1098
  value: Number(v.value),
794
1099
  };
795
1100
  });
@@ -797,7 +1102,7 @@ export class CollectionField {
797
1102
  return options.map((v) => {
798
1103
  return {
799
1104
  ...v,
800
- label: this.flowEngine.translate(v.label, { ns: 'lm-collections' }),
1105
+ label: translateOptionLabel(this.flowEngine, v.label, { ns: 'lm-collections' }),
801
1106
  };
802
1107
  });
803
1108
  }
@@ -835,7 +1140,7 @@ export class CollectionField {
835
1140
  {
836
1141
  ..._.omit(this.options.uiSchema?.['x-component-props'] || {}, 'fieldNames'),
837
1142
  options: this.enum.length ? this.enum : undefined,
838
- mode: this.type === 'array' ? 'multiple' : undefined,
1143
+ mode: this.interface === 'multipleSelect' ? 'multiple' : undefined,
839
1144
  multiple: target ? ['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) : undefined,
840
1145
  maxCount: target && !['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) ? 1 : undefined,
841
1146
  target: target,
@@ -856,7 +1161,21 @@ export class CollectionField {
856
1161
  });
857
1162
 
858
1163
  if (error) {
859
- const message = error.details.map((d: any) => d.message.replace(/"value"/g, `"${label}"`)).join(', ');
1164
+ const message = error.details
1165
+ .map((d: any) => {
1166
+ const translated = this.flowEngine.translate(d.type, {
1167
+ ...d.context,
1168
+ ns: 'data-source-main',
1169
+ label,
1170
+ });
1171
+
1172
+ if (translated && translated !== d.type) {
1173
+ return translated;
1174
+ }
1175
+
1176
+ return d.message.replace(/"value"/g, `"${label}"`);
1177
+ })
1178
+ .join(', ');
860
1179
  return Promise.reject(message);
861
1180
  }
862
1181
 
@@ -879,8 +1198,13 @@ export class CollectionField {
879
1198
  }
880
1199
 
881
1200
  getInterfaceOptions() {
882
- const app = this.flowEngine.context.app;
883
- return app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface(this.interface);
1201
+ const ctx = this.flowEngine.context;
1202
+ return getCollectionFieldInterface(
1203
+ this.interface,
1204
+ this.collection?.dataSource?.dataSourceManager,
1205
+ ctx.dataSourceManager,
1206
+ ctx.app?.dataSourceManager,
1207
+ );
884
1208
  }
885
1209
 
886
1210
  getFilterOperators() {
@@ -158,9 +158,6 @@ export class FlowExecutor {
158
158
  const stepDefaultParams = await resolveDefaultParams(step.defaultParams, runtimeCtx);
159
159
  combinedParams = { ...stepDefaultParams };
160
160
  } else {
161
- flowContext.logger.error(
162
- `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`,
163
- );
164
161
  continue;
165
162
  }
166
163
 
@@ -519,6 +516,12 @@ export class FlowExecutor {
519
516
  result,
520
517
  ...(abortedByExitAll ? { aborted: true } : {}),
521
518
  });
519
+ if (result && typeof result === 'object') {
520
+ Object.defineProperty(result, '__abortedByExitAll', {
521
+ value: abortedByExitAll,
522
+ configurable: true,
523
+ });
524
+ }
522
525
  return result;
523
526
  } catch (error) {
524
527
  // 进入错误钩子并记录
@@ -81,6 +81,32 @@ describe('FlowExecutor', () => {
81
81
  expect(result.step2).toBe('step2-ok');
82
82
  });
83
83
 
84
+ it('runFlow silently skips steps without use or handler', async () => {
85
+ const flows = {
86
+ referenceSettings: {
87
+ steps: {
88
+ target: {},
89
+ },
90
+ },
91
+ } satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
92
+ const model = createModelWithFlows('m-empty-step', flows);
93
+ const loggerChildSpy = vi.spyOn(engine.logger, 'child').mockReturnValue(engine.logger);
94
+ const loggerWarnSpy = vi.spyOn(engine.logger, 'warn').mockImplementation(() => {});
95
+ const loggerErrorSpy = vi.spyOn(engine.logger, 'error').mockImplementation(() => {});
96
+
97
+ try {
98
+ const result = await engine.executor.runFlow(model, 'referenceSettings');
99
+
100
+ expect(result).toEqual({});
101
+ expect(loggerWarnSpy).not.toHaveBeenCalled();
102
+ expect(loggerErrorSpy).not.toHaveBeenCalled();
103
+ } finally {
104
+ loggerChildSpy.mockRestore();
105
+ loggerWarnSpy.mockRestore();
106
+ loggerErrorSpy.mockRestore();
107
+ }
108
+ });
109
+
84
110
  it("dispatchEvent('beforeRender') executes flows in sort order and caches result (when options specify)", async () => {
85
111
  const calls: string[] = [];
86
112
  const mkFlow = (key: string, sort: number) => ({
@@ -232,6 +258,37 @@ describe('FlowExecutor', () => {
232
258
  expect(calls.sort()).toEqual(['a', 'b']);
233
259
  });
234
260
 
261
+ it('dispatchEvent sequential exposes abortedByExitAll metadata on result array', async () => {
262
+ const flows = {
263
+ stopClose: {
264
+ on: { eventName: 'close' },
265
+ steps: {
266
+ only: {
267
+ handler: vi.fn().mockImplementation((ctx) => {
268
+ ctx.exit();
269
+ }),
270
+ },
271
+ },
272
+ },
273
+ afterClose: {
274
+ on: { eventName: 'close', phase: 'afterAllFlows' },
275
+ steps: {
276
+ only: {
277
+ handler: vi.fn(),
278
+ },
279
+ },
280
+ },
281
+ } satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
282
+
283
+ const model = createModelWithFlows('m-close-meta', flows);
284
+
285
+ const result = await engine.executor.dispatchEvent(model, 'close', {}, { sequential: true });
286
+
287
+ expect(Array.isArray(result)).toBe(true);
288
+ expect((result as any).__abortedByExitAll).toBe(true);
289
+ expect(flows.afterClose.steps.only.handler).not.toHaveBeenCalled();
290
+ });
291
+
235
292
  it('dispatchEvent sequential respects sort order and stops on errors', async () => {
236
293
  const calls: string[] = [];
237
294
  const mkFlow = (key: string, sort: number, opts?: { throw?: boolean }) => ({