@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.45

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 (209) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/FlowContextProvider.d.ts +5 -1
  4. package/lib/FlowContextProvider.js +9 -2
  5. package/lib/JSRunner.d.ts +10 -1
  6. package/lib/JSRunner.js +50 -5
  7. package/lib/ViewScopedFlowEngine.js +5 -1
  8. package/lib/components/FieldModelRenderer.js +2 -2
  9. package/lib/components/FlowModelRenderer.d.ts +3 -1
  10. package/lib/components/FlowModelRenderer.js +12 -6
  11. package/lib/components/FormItem.d.ts +6 -0
  12. package/lib/components/FormItem.js +11 -3
  13. package/lib/components/MobilePopup.js +6 -5
  14. package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
  15. package/lib/components/dnd/gridDragPlanner.js +613 -21
  16. package/lib/components/dnd/index.d.ts +31 -2
  17. package/lib/components/dnd/index.js +244 -23
  18. package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
  19. package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
  20. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
  21. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -11
  22. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
  23. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
  24. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
  25. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
  26. package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
  27. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
  28. package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
  29. package/lib/components/subModel/AddSubModelButton.js +27 -1
  30. package/lib/components/subModel/LazyDropdown.js +293 -52
  31. package/lib/components/subModel/index.d.ts +1 -0
  32. package/lib/components/subModel/index.js +19 -0
  33. package/lib/components/subModel/utils.d.ts +1 -1
  34. package/lib/components/subModel/utils.js +9 -3
  35. package/lib/components/variables/VariableHybridInput.d.ts +27 -0
  36. package/lib/components/variables/VariableHybridInput.js +499 -0
  37. package/lib/components/variables/index.d.ts +2 -0
  38. package/lib/components/variables/index.js +3 -0
  39. package/lib/data-source/index.d.ts +84 -0
  40. package/lib/data-source/index.js +259 -5
  41. package/lib/executor/FlowExecutor.js +32 -9
  42. package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
  43. package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
  44. package/lib/flow-registry/index.d.ts +1 -0
  45. package/lib/flow-registry/index.js +3 -1
  46. package/lib/flowContext.d.ts +3 -0
  47. package/lib/flowContext.js +46 -1
  48. package/lib/flowEngine.d.ts +151 -1
  49. package/lib/flowEngine.js +392 -18
  50. package/lib/flowI18n.js +2 -1
  51. package/lib/flowSettings.d.ts +14 -6
  52. package/lib/flowSettings.js +34 -6
  53. package/lib/index.d.ts +2 -0
  54. package/lib/index.js +7 -0
  55. package/lib/lazy-helper.d.ts +14 -0
  56. package/lib/lazy-helper.js +71 -0
  57. package/lib/locale/en-US.json +1 -0
  58. package/lib/locale/index.d.ts +2 -0
  59. package/lib/locale/zh-CN.json +1 -0
  60. package/lib/models/DisplayItemModel.d.ts +1 -1
  61. package/lib/models/EditableItemModel.d.ts +1 -1
  62. package/lib/models/FilterableItemModel.d.ts +1 -1
  63. package/lib/models/flowModel.d.ts +13 -10
  64. package/lib/models/flowModel.js +81 -21
  65. package/lib/provider.js +38 -23
  66. package/lib/reactive/observer.js +46 -16
  67. package/lib/runjs-context/registry.d.ts +1 -1
  68. package/lib/runjs-context/setup.js +20 -12
  69. package/lib/runjs-context/snippets/index.js +13 -2
  70. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
  71. package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
  72. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
  73. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  74. package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
  75. package/lib/scheduler/ModelOperationScheduler.js +3 -2
  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/parsePathnameToViewParams.d.ts +5 -1
  82. package/lib/utils/parsePathnameToViewParams.js +29 -5
  83. package/lib/utils/randomId.d.ts +39 -0
  84. package/lib/utils/randomId.js +45 -0
  85. package/lib/utils/runjsTemplateCompat.js +1 -1
  86. package/lib/utils/runjsValue.js +41 -11
  87. package/lib/utils/schema-utils.d.ts +7 -1
  88. package/lib/utils/schema-utils.js +19 -0
  89. package/lib/views/FlowView.d.ts +7 -1
  90. package/lib/views/FlowView.js +11 -1
  91. package/lib/views/PageComponent.js +8 -6
  92. package/lib/views/ViewNavigation.d.ts +12 -2
  93. package/lib/views/ViewNavigation.js +28 -9
  94. package/lib/views/createViewMeta.js +114 -50
  95. package/lib/views/inheritLayoutContext.d.ts +10 -0
  96. package/lib/views/inheritLayoutContext.js +50 -0
  97. package/lib/views/runViewBeforeClose.d.ts +10 -0
  98. package/lib/views/runViewBeforeClose.js +45 -0
  99. package/lib/views/useDialog.d.ts +2 -1
  100. package/lib/views/useDialog.js +22 -3
  101. package/lib/views/useDrawer.d.ts +2 -1
  102. package/lib/views/useDrawer.js +22 -3
  103. package/lib/views/usePage.d.ts +5 -11
  104. package/lib/views/usePage.js +304 -144
  105. package/package.json +6 -5
  106. package/src/FlowContextProvider.tsx +9 -1
  107. package/src/JSRunner.ts +68 -4
  108. package/src/ViewScopedFlowEngine.ts +4 -0
  109. package/src/__tests__/JSRunner.test.ts +27 -1
  110. package/src/__tests__/createViewMeta.popup.test.ts +115 -1
  111. package/src/__tests__/flow-engine.test.ts +166 -0
  112. package/src/__tests__/flowContext.test.ts +82 -1
  113. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  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 +16 -0
  120. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  121. package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
  122. package/src/__tests__/runjsSnippets.test.ts +21 -0
  123. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  124. package/src/components/FieldModelRenderer.tsx +2 -1
  125. package/src/components/FlowModelRenderer.tsx +18 -6
  126. package/src/components/FormItem.tsx +7 -1
  127. package/src/components/MobilePopup.tsx +4 -2
  128. package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
  129. package/src/components/__tests__/FormItem.test.tsx +25 -0
  130. package/src/components/__tests__/dnd.test.ts +44 -0
  131. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
  132. package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
  133. package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
  134. package/src/components/dnd/gridDragPlanner.ts +758 -19
  135. package/src/components/dnd/index.tsx +305 -28
  136. package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
  137. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +99 -11
  138. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
  139. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
  140. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +194 -5
  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 +32 -2
  145. package/src/components/subModel/LazyDropdown.tsx +332 -56
  146. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +522 -37
  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 +7 -1
  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 +68 -1
  154. package/src/data-source/index.ts +322 -6
  155. package/src/executor/FlowExecutor.ts +35 -10
  156. package/src/executor/__tests__/flowExecutor.test.ts +85 -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 +50 -3
  161. package/src/flowEngine.ts +449 -14
  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__/dispatchEvent.when.test.ts +214 -0
  172. package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
  173. package/src/models/__tests__/flowModel.test.ts +80 -37
  174. package/src/models/flowModel.tsx +122 -36
  175. package/src/provider.tsx +41 -25
  176. package/src/reactive/__tests__/observer.test.tsx +82 -0
  177. package/src/reactive/observer.tsx +87 -25
  178. package/src/runjs-context/registry.ts +1 -1
  179. package/src/runjs-context/setup.ts +22 -12
  180. package/src/runjs-context/snippets/index.ts +12 -1
  181. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  182. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  183. package/src/scheduler/ModelOperationScheduler.ts +14 -3
  184. package/src/types.ts +62 -0
  185. package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
  186. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +28 -0
  187. package/src/utils/__tests__/runjsValue.test.ts +11 -0
  188. package/src/utils/__tests__/utils.test.ts +62 -0
  189. package/src/utils/createCollectionContextMeta.ts +6 -2
  190. package/src/utils/index.ts +5 -1
  191. package/src/utils/parsePathnameToViewParams.ts +47 -7
  192. package/src/utils/randomId.ts +48 -0
  193. package/src/utils/runjsTemplateCompat.ts +1 -1
  194. package/src/utils/runjsValue.ts +50 -11
  195. package/src/utils/schema-utils.ts +30 -1
  196. package/src/views/FlowView.tsx +22 -2
  197. package/src/views/PageComponent.tsx +7 -4
  198. package/src/views/ViewNavigation.ts +46 -9
  199. package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
  200. package/src/views/__tests__/ViewNavigation.test.ts +52 -0
  201. package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
  202. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  203. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
  204. package/src/views/createViewMeta.ts +106 -34
  205. package/src/views/inheritLayoutContext.ts +26 -0
  206. package/src/views/runViewBeforeClose.ts +19 -0
  207. package/src/views/useDialog.tsx +27 -3
  208. package/src/views/useDrawer.tsx +27 -3
  209. package/src/views/usePage.tsx +367 -179
@@ -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
 
@@ -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,187 @@ 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;
85
337
  }
86
338
 
87
339
  export class DataSource {
@@ -110,6 +362,14 @@ export class DataSource {
110
362
  return this.options.key;
111
363
  }
112
364
 
365
+ get status() {
366
+ return this.options.status;
367
+ }
368
+
369
+ get errorMessage() {
370
+ return this.options.errorMessage;
371
+ }
372
+
113
373
  setDataSourceManager(dataSourceManager: DataSourceManager) {
114
374
  this.dataSourceManager = dataSourceManager;
115
375
  }
@@ -149,6 +409,10 @@ export class DataSource {
149
409
  return this.collectionManager.upsertCollections(collections, options);
150
410
  }
151
411
 
412
+ setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) {
413
+ return this.collectionManager.setCollections(collections, options);
414
+ }
415
+
152
416
  removeCollection(name: string) {
153
417
  return this.collectionManager.removeCollection(name);
154
418
  }
@@ -162,6 +426,14 @@ export class DataSource {
162
426
  Object.assign(this.options, newOptions);
163
427
  }
164
428
 
429
+ patchOptions(newOptions: any = {}) {
430
+ Object.assign(this.options, newOptions);
431
+ }
432
+
433
+ reload() {
434
+ return this.dataSourceManager.reloadDataSource(this.key);
435
+ }
436
+
165
437
  getCollectionField(fieldPath: string) {
166
438
  const [collectionName, ...otherKeys] = fieldPath.split('.');
167
439
  const fieldName = otherKeys.join('.');
@@ -194,6 +466,11 @@ export class CollectionManager {
194
466
  this.collections = observable.shallow<Map<string, Collection>>(new Map());
195
467
  }
196
468
 
469
+ protected resetCaches() {
470
+ this.childrenCollectionsName = {};
471
+ this.allCollectionsInheritChain = undefined;
472
+ }
473
+
197
474
  get flowEngine() {
198
475
  return this.dataSource.flowEngine;
199
476
  }
@@ -208,10 +485,12 @@ export class CollectionManager {
208
485
  col.setDataSource(this.dataSource);
209
486
  col.initInherits();
210
487
  this.collections.set(col.name, col);
488
+ this.resetCaches();
211
489
  }
212
490
 
213
491
  removeCollection(name: string) {
214
492
  this.collections.delete(name);
493
+ this.resetCaches();
215
494
  }
216
495
 
217
496
  updateCollection(newOptions: CollectionOptions, options: { clearFields?: boolean } = {}) {
@@ -220,6 +499,7 @@ export class CollectionManager {
220
499
  throw new Error(`Collection ${newOptions.name} not found`);
221
500
  }
222
501
  collection.setOptions(newOptions, options);
502
+ this.resetCaches();
223
503
  }
224
504
 
225
505
  upsertCollection(options: CollectionOptions) {
@@ -239,6 +519,12 @@ export class CollectionManager {
239
519
  this.addCollection(collection);
240
520
  }
241
521
  }
522
+ this.resetCaches();
523
+ }
524
+
525
+ setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) {
526
+ this.clearCollections();
527
+ this.upsertCollections(collections, options);
242
528
  }
243
529
 
244
530
  sortCollectionsByInherits(collections: CollectionOptions[]): CollectionOptions[] {
@@ -315,6 +601,7 @@ export class CollectionManager {
315
601
 
316
602
  clearCollections() {
317
603
  this.collections.clear();
604
+ this.resetCaches();
318
605
  }
319
606
 
320
607
  getAssociation(associationName: string): CollectionField | undefined {
@@ -501,6 +788,12 @@ export class Collection {
501
788
 
502
789
  get titleCollectionField() {
503
790
  const titleFieldName = this.options.titleField || this.filterTargetKey;
791
+ if (Array.isArray(titleFieldName)) {
792
+ if (titleFieldName.length !== 1) {
793
+ return undefined;
794
+ }
795
+ return this.getField(titleFieldName[0]);
796
+ }
504
797
  const titleCollectionField = this.getField(titleFieldName);
505
798
  return titleCollectionField;
506
799
  }
@@ -531,6 +824,10 @@ export class Collection {
531
824
  this.upsertFields(this.options.fields || []);
532
825
  }
533
826
 
827
+ setOption(key: string, value: any) {
828
+ this.options[key] = value;
829
+ }
830
+
534
831
  getFields(): CollectionField[] {
535
832
  // 合并自身 fields 和所有 inherits 的 fields,后者优先被覆盖
536
833
  const fieldMap = new Map<string, CollectionField>();
@@ -835,7 +1132,7 @@ export class CollectionField {
835
1132
  {
836
1133
  ..._.omit(this.options.uiSchema?.['x-component-props'] || {}, 'fieldNames'),
837
1134
  options: this.enum.length ? this.enum : undefined,
838
- mode: this.type === 'array' ? 'multiple' : undefined,
1135
+ mode: this.interface === 'multipleSelect' ? 'multiple' : undefined,
839
1136
  multiple: target ? ['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) : undefined,
840
1137
  maxCount: target && !['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) ? 1 : undefined,
841
1138
  target: target,
@@ -856,7 +1153,21 @@ export class CollectionField {
856
1153
  });
857
1154
 
858
1155
  if (error) {
859
- const message = error.details.map((d: any) => d.message.replace(/"value"/g, `"${label}"`)).join(', ');
1156
+ const message = error.details
1157
+ .map((d: any) => {
1158
+ const translated = this.flowEngine.translate(d.type, {
1159
+ ...d.context,
1160
+ ns: 'data-source-main',
1161
+ label,
1162
+ });
1163
+
1164
+ if (translated && translated !== d.type) {
1165
+ return translated;
1166
+ }
1167
+
1168
+ return d.message.replace(/"value"/g, `"${label}"`);
1169
+ })
1170
+ .join(', ');
860
1171
  return Promise.reject(message);
861
1172
  }
862
1173
 
@@ -879,8 +1190,13 @@ export class CollectionField {
879
1190
  }
880
1191
 
881
1192
  getInterfaceOptions() {
882
- const app = this.flowEngine.context.app;
883
- return app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface(this.interface);
1193
+ const ctx = this.flowEngine.context;
1194
+ return getCollectionFieldInterface(
1195
+ this.interface,
1196
+ this.collection?.dataSource?.dataSourceManager,
1197
+ ctx.dataSourceManager,
1198
+ ctx.app?.dataSourceManager,
1199
+ );
884
1200
  }
885
1201
 
886
1202
  getFilterOperators() {
@@ -158,7 +158,7 @@ export class FlowExecutor {
158
158
  const stepDefaultParams = await resolveDefaultParams(step.defaultParams, runtimeCtx);
159
159
  combinedParams = { ...stepDefaultParams };
160
160
  } else {
161
- flowContext.logger.error(
161
+ flowContext.logger.warn(
162
162
  `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`,
163
163
  );
164
164
  continue;
@@ -224,10 +224,12 @@ export class FlowExecutor {
224
224
  await this.emitModelEventIf(eventName, `flow:${flowKey}:step:${stepKey}:end`, {
225
225
  ...flowEventBasePayload,
226
226
  stepKey,
227
+ aborted: true,
227
228
  });
228
229
  await this.emitModelEventIf(eventName, `flow:${flowKey}:end`, {
229
230
  ...flowEventBasePayload,
230
231
  result: error,
232
+ aborted: true,
231
233
  });
232
234
  return Promise.resolve(error);
233
235
  }
@@ -316,9 +318,9 @@ export class FlowExecutor {
316
318
 
317
319
  // 记录本次 dispatchEvent 内注册的调度任务,用于在结束/错误后兜底清理未触发的任务
318
320
  const scheduledCancels: ScheduledCancel[] = [];
319
-
320
- // 组装执行函数(返回值用于缓存;beforeRender 返回 results:any[],其它返回 true)
321
- const execute = async () => {
321
+ // 组装执行函数(返回值用于缓存,包含事件结果与是否被 exitAll 中止)
322
+ const execute = async (): Promise<{ result: any[]; abortedByExitAll: boolean }> => {
323
+ let abortedByExitAll = false;
322
324
  if (sequential) {
323
325
  // 顺序执行:动态流(实例级)优先,其次静态流;各自组内再按 sort 升序,最后保持原始顺序稳定
324
326
  const flowsWithIndex = flowsToRun.map((f, i) => ({ f, i }));
@@ -351,7 +353,7 @@ export class FlowExecutor {
351
353
  .map((f) => [f.key, f] as const),
352
354
  );
353
355
  const scheduled = new Set<string>();
354
- const scheduleGroups = new Map<string, Array<{ flow: any; order: number }>>();
356
+ const scheduleGroups = new Map<string, Array<{ flow: any; order: number; shouldSkipOnAborted: boolean }>>();
355
357
  ordered.forEach((flow, indexInOrdered) => {
356
358
  const on = flow.on;
357
359
  const onObj = typeof on === 'object' ? (on as any) : undefined;
@@ -402,9 +404,11 @@ export class FlowExecutor {
402
404
  }
403
405
 
404
406
  if (!whenKey) return;
407
+ const shouldSkipOnAborted =
408
+ whenKey === `event:${eventName}:end` || phase === 'afterFlow' || phase === 'afterStep';
405
409
  scheduled.add(flow.key);
406
410
  const list = scheduleGroups.get(whenKey) || [];
407
- list.push({ flow, order: indexInOrdered });
411
+ list.push({ flow, order: indexInOrdered, shouldSkipOnAborted });
408
412
  scheduleGroups.set(whenKey, list);
409
413
  });
410
414
 
@@ -417,6 +421,14 @@ export class FlowExecutor {
417
421
  return a.order - b.order;
418
422
  });
419
423
  for (const it of sorted) {
424
+ const when = it.shouldSkipOnAborted
425
+ ? Object.assign(
426
+ (event: { type: string; aborted?: boolean }) => event.type === whenKey && event.aborted !== true,
427
+ {
428
+ __eventType: whenKey,
429
+ },
430
+ )
431
+ : (whenKey as any);
420
432
  const cancel = model.scheduleModelOperation(
421
433
  model.uid,
422
434
  async (m) => {
@@ -426,7 +438,7 @@ export class FlowExecutor {
426
438
  }
427
439
  results.push(res);
428
440
  },
429
- { when: whenKey as any },
441
+ { when },
430
442
  );
431
443
  scheduledCancels.push(cancel);
432
444
  }
@@ -441,12 +453,14 @@ export class FlowExecutor {
441
453
  const result = await this.runFlow(model, flow.key, inputArgs, runId, eventName);
442
454
  if (result instanceof FlowExitAllException) {
443
455
  logger.debug(`[FlowEngine.dispatchEvent] ${result.message}`);
456
+ abortedByExitAll = true;
444
457
  break; // 终止后续
445
458
  }
446
459
  results.push(result);
447
460
  } catch (error) {
448
461
  if (error instanceof FlowExitAllException) {
449
462
  logger.debug(`[FlowEngine.dispatchEvent] ${error.message}`);
463
+ abortedByExitAll = true;
450
464
  break; // 终止后续
451
465
  }
452
466
  logger.error(
@@ -456,7 +470,7 @@ export class FlowExecutor {
456
470
  throw error;
457
471
  }
458
472
  }
459
- return results;
473
+ return { result: results, abortedByExitAll };
460
474
  }
461
475
 
462
476
  // 并行
@@ -475,7 +489,11 @@ export class FlowExecutor {
475
489
  }
476
490
  }),
477
491
  );
478
- return results.filter((x) => x !== undefined);
492
+ const filteredResults = results.filter((x) => x !== undefined);
493
+ if (filteredResults.some((x) => x instanceof FlowExitAllException)) {
494
+ abortedByExitAll = true;
495
+ }
496
+ return { result: filteredResults, abortedByExitAll };
479
497
  };
480
498
 
481
499
  // 缓存键:按事件+scope 统一管理(beforeRender 也使用事件名 beforeRender)
@@ -489,7 +507,7 @@ export class FlowExecutor {
489
507
  : null;
490
508
 
491
509
  try {
492
- const result = await this.withApplyFlowCache(cacheKey, execute);
510
+ const { result, abortedByExitAll } = await this.withApplyFlowCache(cacheKey, execute);
493
511
  // 事件结束钩子
494
512
  try {
495
513
  await model.onDispatchEventEnd?.(eventName, options, inputArgs, result);
@@ -499,7 +517,14 @@ export class FlowExecutor {
499
517
  await this.emitModelEventIf(eventName, 'end', {
500
518
  ...eventBasePayload,
501
519
  result,
520
+ ...(abortedByExitAll ? { aborted: true } : {}),
502
521
  });
522
+ if (result && typeof result === 'object') {
523
+ Object.defineProperty(result, '__abortedByExitAll', {
524
+ value: abortedByExitAll,
525
+ configurable: true,
526
+ });
527
+ }
503
528
  return result;
504
529
  } catch (error) {
505
530
  // 进入错误钩子并记录