@nocobase/flow-engine 2.0.0-alpha.36 → 2.0.0-alpha.37

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.
@@ -82,6 +82,7 @@ export declare class CollectionManager {
82
82
  getCollections(): Collection[];
83
83
  clearCollections(): void;
84
84
  getAssociation(associationName: string): CollectionField | undefined;
85
+ getChildrenCollections(name: any): any[];
85
86
  }
86
87
  export declare class Collection {
87
88
  fields: Map<string, CollectionField>;
@@ -300,6 +300,23 @@ const _CollectionManager = class _CollectionManager {
300
300
  }
301
301
  return collection.getField(fieldName);
302
302
  }
303
+ getChildrenCollections(name) {
304
+ const childrens = [];
305
+ const collections = Array.from(this.collections.values());
306
+ const getChildrens = /* @__PURE__ */ __name((name2) => {
307
+ const inheritCollections = collections.filter((v) => {
308
+ var _a;
309
+ return (_a = v.options.inherits) == null ? void 0 : _a.includes(name2);
310
+ });
311
+ inheritCollections.forEach((v) => {
312
+ const collectionKey = v.name;
313
+ childrens.push(v);
314
+ return getChildrens(collectionKey);
315
+ });
316
+ return childrens;
317
+ }, "getChildrens");
318
+ return getChildrens(name);
319
+ }
303
320
  };
304
321
  __name(_CollectionManager, "CollectionManager");
305
322
  let CollectionManager = _CollectionManager;
@@ -228,7 +228,21 @@ const _FlowExecutor = class _FlowExecutor {
228
228
  });
229
229
  const execute = /* @__PURE__ */ __name(async () => {
230
230
  if (sequential) {
231
- const ordered = flows.slice().sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
231
+ const flowsWithIndex = flows.map((f, i) => ({ f, i }));
232
+ const ordered = flowsWithIndex.slice().sort((a, b) => {
233
+ var _a2, _b2;
234
+ const regA = a.f["flowRegistry"];
235
+ const regB = b.f["flowRegistry"];
236
+ const typeA = (_a2 = regA == null ? void 0 : regA.constructor) == null ? void 0 : _a2._type;
237
+ const typeB = (_b2 = regB == null ? void 0 : regB.constructor) == null ? void 0 : _b2._type;
238
+ const groupA = typeA === "instance" ? 0 : 1;
239
+ const groupB = typeB === "instance" ? 0 : 1;
240
+ if (groupA !== groupB) return groupA - groupB;
241
+ const sa = a.f.sort ?? 0;
242
+ const sb = b.f.sort ?? 0;
243
+ if (sa !== sb) return sa - sb;
244
+ return a.i - b.i;
245
+ }).map((x) => x.f);
232
246
  const results2 = [];
233
247
  for (const flow of ordered) {
234
248
  try {
@@ -12,6 +12,7 @@ import { BaseFlowRegistry } from './BaseFlowRegistry';
12
12
  type FlowKey = string;
13
13
  export declare class GlobalFlowRegistry extends BaseFlowRegistry {
14
14
  protected target: ModelConstructor;
15
+ static readonly _type: "global";
15
16
  constructor(target: ModelConstructor);
16
17
  removeFlow(flowKey: FlowKey): void;
17
18
  getFlow(flowKey: FlowKey): FlowDefinition | undefined;
@@ -11,6 +11,7 @@ var __defProp = Object.defineProperty;
11
11
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
12
  var __getOwnPropNames = Object.getOwnPropertyNames;
13
13
  var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14
15
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
15
16
  var __export = (target, all) => {
16
17
  for (var name in all)
@@ -25,6 +26,7 @@ var __copyProps = (to, from, except, desc) => {
25
26
  return to;
26
27
  };
27
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
28
30
  var GlobalFlowRegistry_exports = {};
29
31
  __export(GlobalFlowRegistry_exports, {
30
32
  GlobalFlowRegistry: () => GlobalFlowRegistry
@@ -88,6 +90,7 @@ const _GlobalFlowRegistry = class _GlobalFlowRegistry extends import_BaseFlowReg
88
90
  }
89
91
  };
90
92
  __name(_GlobalFlowRegistry, "GlobalFlowRegistry");
93
+ __publicField(_GlobalFlowRegistry, "_type", "global");
91
94
  let GlobalFlowRegistry = _GlobalFlowRegistry;
92
95
  // Annotate the CommonJS export names for ESM import in node:
93
96
  0 && (module.exports = {
@@ -12,6 +12,7 @@ import { BaseFlowRegistry } from './BaseFlowRegistry';
12
12
  type FlowKey = string;
13
13
  export declare class InstanceFlowRegistry extends BaseFlowRegistry {
14
14
  protected model: FlowModel;
15
+ static readonly _type: "instance";
15
16
  constructor(model: FlowModel);
16
17
  save(): Promise<void>;
17
18
  saveFlow(flow: FlowDefinition): Promise<void>;
@@ -11,6 +11,7 @@ var __defProp = Object.defineProperty;
11
11
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
12
  var __getOwnPropNames = Object.getOwnPropertyNames;
13
13
  var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14
15
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
15
16
  var __export = (target, all) => {
16
17
  for (var name in all)
@@ -25,6 +26,7 @@ var __copyProps = (to, from, except, desc) => {
25
26
  return to;
26
27
  };
27
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
28
30
  var InstanceFlowRegistry_exports = {};
29
31
  __export(InstanceFlowRegistry_exports, {
30
32
  InstanceFlowRegistry: () => InstanceFlowRegistry
@@ -52,6 +54,7 @@ const _InstanceFlowRegistry = class _InstanceFlowRegistry extends import_BaseFlo
52
54
  }
53
55
  };
54
56
  __name(_InstanceFlowRegistry, "InstanceFlowRegistry");
57
+ __publicField(_InstanceFlowRegistry, "_type", "instance");
55
58
  let InstanceFlowRegistry = _InstanceFlowRegistry;
56
59
  // Annotate the CommonJS export names for ESM import in node:
57
60
  0 && (module.exports = {
@@ -146,6 +146,9 @@ declare class BaseFlowEngineContext extends FlowContext {
146
146
  requireAsync: (url: string) => Promise<any>;
147
147
  importAsync: (url: string) => Promise<any>;
148
148
  createJSRunner: (options?: JSRunnerOptions) => JSRunner;
149
+ pageInfo: {
150
+ version?: 'v1' | 'v2';
151
+ };
149
152
  /**
150
153
  * @deprecated use `resolveJsonTemplate` instead
151
154
  */
@@ -532,14 +532,15 @@ const _FlowModel = class _FlowModel {
532
532
  const instanceKeys = new Set(instanceFlows.keys());
533
533
  const staticEntries = Array.from(staticFlows.entries()).filter(([key]) => !instanceKeys.has(key));
534
534
  const instanceEntries = Array.from(instanceFlows.entries());
535
- const allEntries = [...staticEntries, ...instanceEntries];
536
- allEntries.sort(([, a], [, b]) => {
537
- const sa = a.sort ?? 0;
538
- const sb = b.sort ?? 0;
535
+ const instanceEntriesWithIndex = instanceEntries.map((e, i) => ({ e, i }));
536
+ instanceEntriesWithIndex.sort((a, b) => {
537
+ const sa = a.e[1].sort ?? 0;
538
+ const sb = b.e[1].sort ?? 0;
539
539
  if (sa !== sb) return sa - sb;
540
- return 0;
540
+ return a.i - b.i;
541
541
  });
542
- return new Map(allEntries);
542
+ const merged = [...instanceEntriesWithIndex.map(({ e }) => e), ...staticEntries];
543
+ return new Map(merged);
543
544
  }
544
545
  setProps(props, value) {
545
546
  if (typeof props === "string") {
@@ -611,7 +612,7 @@ const _FlowModel = class _FlowModel {
611
612
  }
612
613
  async dispatchEvent(eventName, inputArgs, options) {
613
614
  const isBeforeRender = eventName === "beforeRender";
614
- const defaults = isBeforeRender ? { sequential: true, useCache: true } : {};
615
+ const defaults = isBeforeRender ? { sequential: true, useCache: true } : { sequential: true };
615
616
  const execOptions = {
616
617
  sequential: (options == null ? void 0 : options.sequential) ?? defaults.sequential,
617
618
  useCache: (options == null ? void 0 : options.useCache) ?? defaults.useCache
@@ -0,0 +1,10 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { FlowDefinition } from '../FlowDefinition';
10
+ export declare const isBeforeRenderFlow: (flow: FlowDefinition) => boolean;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var flows_exports = {};
29
+ __export(flows_exports, {
30
+ isBeforeRenderFlow: () => isBeforeRenderFlow
31
+ });
32
+ module.exports = __toCommonJS(flows_exports);
33
+ const isBeforeRenderFlow = /* @__PURE__ */ __name((flow) => {
34
+ if (typeof flow.on === "string") {
35
+ return flow.on === "beforeRender";
36
+ }
37
+ if (typeof flow.on === "object") {
38
+ return flow.on.eventName === "beforeRender";
39
+ }
40
+ if (!flow.on && flow.manual !== true) {
41
+ return true;
42
+ }
43
+ return false;
44
+ }, "isBeforeRenderFlow");
45
+ // Annotate the CommonJS export names for ESM import in node:
46
+ 0 && (module.exports = {
47
+ isBeforeRenderFlow
48
+ });
@@ -22,4 +22,6 @@ export { clearAutoFlowError, getAutoFlowError, setAutoFlowError, type AutoFlowEr
22
22
  export { parsePathnameToViewParams, type ViewParam } from './parsePathnameToViewParams';
23
23
  export { buildSettingsViewInputArgs } from './buildSettingsViewInputArgs';
24
24
  export { createSafeDocument, createSafeWindow, createSafeNavigator } from './safeGlobals';
25
+ export { createEphemeralContext } from './createEphemeralContext';
25
26
  export { pruneFilter } from './pruneFilter';
27
+ export { isBeforeRenderFlow } from './flows';
@@ -40,6 +40,7 @@ __export(utils_exports, {
40
40
  createAssociationSubpathResolver: () => import_associationObjectVariable.createAssociationSubpathResolver,
41
41
  createCollectionContextMeta: () => import_createCollectionContextMeta.createCollectionContextMeta,
42
42
  createCurrentRecordMetaFactory: () => import_variablesParams.createCurrentRecordMetaFactory,
43
+ createEphemeralContext: () => import_createEphemeralContext.createEphemeralContext,
43
44
  createRecordMetaFactory: () => import_variablesParams.createRecordMetaFactory,
44
45
  createSafeDocument: () => import_safeGlobals.createSafeDocument,
45
46
  createSafeNavigator: () => import_safeGlobals.createSafeNavigator,
@@ -53,6 +54,7 @@ __export(utils_exports, {
53
54
  getAutoFlowError: () => import_autoFlowError.getAutoFlowError,
54
55
  getT: () => import_translation.getT,
55
56
  inferRecordRef: () => import_variablesParams.inferRecordRef,
57
+ isBeforeRenderFlow: () => import_flows.isBeforeRenderFlow,
56
58
  isInheritedFrom: () => import_inheritance.isInheritedFrom,
57
59
  isVariableExpression: () => import_context.isVariableExpression,
58
60
  parsePathnameToViewParams: () => import_parsePathnameToViewParams.parsePathnameToViewParams,
@@ -83,7 +85,9 @@ var import_autoFlowError = require("./autoFlowError");
83
85
  var import_parsePathnameToViewParams = require("./parsePathnameToViewParams");
84
86
  var import_buildSettingsViewInputArgs = require("./buildSettingsViewInputArgs");
85
87
  var import_safeGlobals = require("./safeGlobals");
88
+ var import_createEphemeralContext = require("./createEphemeralContext");
86
89
  var import_pruneFilter = require("./pruneFilter");
90
+ var import_flows = require("./flows");
87
91
  // Annotate the CommonJS export names for ESM import in node:
88
92
  0 && (module.exports = {
89
93
  BLOCK_GROUP_CONFIGS,
@@ -100,6 +104,7 @@ var import_pruneFilter = require("./pruneFilter");
100
104
  createAssociationSubpathResolver,
101
105
  createCollectionContextMeta,
102
106
  createCurrentRecordMetaFactory,
107
+ createEphemeralContext,
103
108
  createRecordMetaFactory,
104
109
  createSafeDocument,
105
110
  createSafeNavigator,
@@ -113,6 +118,7 @@ var import_pruneFilter = require("./pruneFilter");
113
118
  getAutoFlowError,
114
119
  getT,
115
120
  inferRecordRef,
121
+ isBeforeRenderFlow,
116
122
  isInheritedFrom,
117
123
  isVariableExpression,
118
124
  parsePathnameToViewParams,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.0-alpha.36",
3
+ "version": "2.0.0-alpha.37",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.0.0-alpha.36",
12
- "@nocobase/shared": "2.0.0-alpha.36",
11
+ "@nocobase/sdk": "2.0.0-alpha.37",
12
+ "@nocobase/shared": "2.0.0-alpha.37",
13
13
  "ahooks": "^3.7.2",
14
14
  "dompurify": "^3.0.2",
15
15
  "lodash": "^4.x",
@@ -35,5 +35,5 @@
35
35
  ],
36
36
  "author": "NocoBase Team",
37
37
  "license": "AGPL-3.0",
38
- "gitHead": "c1170bac3ba2325b7420ce65e732c1ba8bbb7767"
38
+ "gitHead": "56e2dc3a83b8ca7afa97cfa71b37a43f2ec10396"
39
39
  }
@@ -321,6 +321,23 @@ export class CollectionManager {
321
321
  }
322
322
  return collection.getField(fieldName);
323
323
  }
324
+
325
+ getChildrenCollections(name) {
326
+ const childrens = [];
327
+ const collections = Array.from(this.collections.values());
328
+ const getChildrens = (name) => {
329
+ const inheritCollections = collections.filter((v: any) => {
330
+ return v.options.inherits?.includes(name);
331
+ });
332
+ inheritCollections.forEach((v) => {
333
+ const collectionKey = v.name;
334
+ childrens.push(v);
335
+ return getChildrens(collectionKey);
336
+ });
337
+ return childrens;
338
+ };
339
+ return getChildrens(name);
340
+ }
324
341
  }
325
342
 
326
343
  // Collection 负责管理自己的 Field
@@ -240,7 +240,24 @@ export class FlowExecutor {
240
240
  // 组装执行函数(返回值用于缓存;beforeRender 返回 results:any[],其它返回 true)
241
241
  const execute = async () => {
242
242
  if (sequential) {
243
- const ordered = flows.slice().sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
243
+ // 顺序执行:动态流(实例级)优先,其次静态流;各自组内再按 sort 升序,最后保持原始顺序稳定
244
+ const flowsWithIndex = flows.map((f, i) => ({ f, i }));
245
+ const ordered = flowsWithIndex
246
+ .slice()
247
+ .sort((a, b) => {
248
+ const regA = a.f['flowRegistry'] as any;
249
+ const regB = b.f['flowRegistry'] as any;
250
+ const typeA = regA?.constructor?._type as 'instance' | 'global' | undefined;
251
+ const typeB = regB?.constructor?._type as 'instance' | 'global' | undefined;
252
+ const groupA = typeA === 'instance' ? 0 : 1; // 实例=0,静态=1
253
+ const groupB = typeB === 'instance' ? 0 : 1;
254
+ if (groupA !== groupB) return groupA - groupB;
255
+ const sa = a.f.sort ?? 0;
256
+ const sb = b.f.sort ?? 0;
257
+ if (sa !== sb) return sa - sb;
258
+ return a.i - b.i; // 稳定排序:保持原有相对顺序
259
+ })
260
+ .map((x) => x.f);
244
261
  const results: any[] = [];
245
262
  for (const flow of ordered) {
246
263
  try {
@@ -15,6 +15,7 @@ import { BaseFlowRegistry } from './BaseFlowRegistry';
15
15
  type FlowKey = string;
16
16
 
17
17
  export class GlobalFlowRegistry extends BaseFlowRegistry {
18
+ static readonly _type = 'global' as const;
18
19
  constructor(protected target: ModelConstructor) {
19
20
  super();
20
21
  }
@@ -14,6 +14,7 @@ import { BaseFlowRegistry } from './BaseFlowRegistry';
14
14
  type FlowKey = string;
15
15
 
16
16
  export class InstanceFlowRegistry extends BaseFlowRegistry {
17
+ static readonly _type = 'instance' as const;
17
18
  constructor(protected model: FlowModel) {
18
19
  super();
19
20
  }
@@ -138,4 +138,58 @@ describe('GlobalFlowRegistry', () => {
138
138
  expect(staticHandler).toHaveBeenCalled();
139
139
  expect(instanceHandler).toHaveBeenCalled();
140
140
  });
141
+
142
+ test('getFlows returns instance (dynamic) flows before static flows with own internal ordering', () => {
143
+ const engine = new FlowEngine();
144
+ class OrderModel extends FlowModel {}
145
+ engine.registerModels({ OrderModel });
146
+
147
+ // Register static flows with various sort values
148
+ OrderModel.registerFlow({ key: 'staticLow', title: 'S-low', sort: -10, steps: {} });
149
+ OrderModel.registerFlow({ key: 'staticHigh', title: 'S-high', sort: 10, steps: {} });
150
+
151
+ const model = engine.createModel({ use: 'OrderModel' });
152
+
153
+ // Add instance flows with sort values that would normally interleave if sorted purely by sort
154
+ model.flowRegistry.addFlow('instMid', { title: 'I-mid', sort: 0, steps: {} });
155
+ model.flowRegistry.addFlow('instHigh', { title: 'I-high', sort: 5, steps: {} });
156
+
157
+ const keys = Array.from(model.getFlows().keys());
158
+ // Expect dynamic flows first (by sort inside group), then static flows (by their own ordering)
159
+ expect(keys).toEqual(['instMid', 'instHigh', 'staticLow', 'staticHigh']);
160
+ });
161
+
162
+ test('sequential dispatch runs instance flows before static flows regardless of static sort', async () => {
163
+ const engine = new FlowEngine();
164
+ class SeqModel extends FlowModel {}
165
+ engine.registerModels({ SeqModel });
166
+
167
+ const calls: string[] = [];
168
+
169
+ // Static flow with very low sort to try to run first if not grouped
170
+ SeqModel.registerFlow({
171
+ key: 'S',
172
+ on: { eventName: 'click' },
173
+ sort: -100,
174
+ steps: { s: { handler: async () => void calls.push('static') } as any },
175
+ });
176
+
177
+ const model = engine.createModel({ use: 'SeqModel' });
178
+
179
+ // Instance flows with sort greater than static
180
+ model.flowRegistry.addFlow('I1', {
181
+ on: { eventName: 'click' },
182
+ sort: 0,
183
+ steps: { i1: { handler: async () => void calls.push('inst1') } as any },
184
+ });
185
+ model.flowRegistry.addFlow('I2', {
186
+ on: { eventName: 'click' },
187
+ sort: 5,
188
+ steps: { i2: { handler: async () => void calls.push('inst2') } as any },
189
+ });
190
+
191
+ await model.dispatchEvent('click', undefined, { sequential: true });
192
+
193
+ expect(calls).toEqual(['inst1', 'inst2', 'static']);
194
+ });
141
195
  });
@@ -896,6 +896,7 @@ class BaseFlowEngineContext extends FlowContext {
896
896
  declare requireAsync: (url: string) => Promise<any>;
897
897
  declare importAsync: (url: string) => Promise<any>;
898
898
  declare createJSRunner: (options?: JSRunnerOptions) => JSRunner;
899
+ declare pageInfo: { version?: 'v1' | 'v2' };
899
900
  /**
900
901
  * @deprecated use `resolveJsonTemplate` instead
901
902
  */
@@ -0,0 +1,169 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, test, expect } from 'vitest';
11
+ import { FlowEngine } from '../../flowEngine';
12
+ import { FlowModel } from '../../models/flowModel';
13
+
14
+ describe('dispatchEvent behavior (defaults, parallel, errors)', () => {
15
+ test('non-beforeRender defaults to sequential execution and respects getEventFlows order', async () => {
16
+ const engine = new FlowEngine();
17
+ class M extends FlowModel {}
18
+ engine.registerModels({ M });
19
+
20
+ const calls: string[] = [];
21
+
22
+ // Static flow with higher sort
23
+ M.registerFlow({
24
+ key: 'S',
25
+ on: { eventName: 'go' },
26
+ sort: 10,
27
+ steps: { s: { handler: async () => void calls.push('static') } as any },
28
+ });
29
+
30
+ const model = engine.createModel({ use: 'M' });
31
+
32
+ // Instance flows
33
+ model.registerFlow('I1', {
34
+ on: { eventName: 'go' },
35
+ sort: 0,
36
+ steps: { i1: { handler: async () => void calls.push('inst1') } as any },
37
+ });
38
+ model.registerFlow('I2', {
39
+ on: { eventName: 'go' },
40
+ sort: 5,
41
+ steps: { i2: { handler: async () => void calls.push('inst2') } as any },
42
+ });
43
+
44
+ // No options provided -> should default to sequential execution
45
+ await model.dispatchEvent('go');
46
+
47
+ // Expected order should match getEventFlows('go') order
48
+ const expected = model.getEventFlows('go').map((f) => f.key);
49
+ expect(calls).toEqual(expected.map((k) => (k === 'I1' ? 'inst1' : k === 'I2' ? 'inst2' : 'static')));
50
+ });
51
+
52
+ test('parallel mode (sequential=false) runs all and tolerates non-beforeRender errors', async () => {
53
+ const engine = new FlowEngine();
54
+ class M extends FlowModel {}
55
+ engine.registerModels({ M });
56
+
57
+ const calls: string[] = [];
58
+
59
+ M.registerFlow({
60
+ key: 'S',
61
+ on: { eventName: 'go' },
62
+ sort: 1,
63
+ steps: { s: { handler: async () => 'static-ok' } as any },
64
+ });
65
+
66
+ const model = engine.createModel({ use: 'M' });
67
+
68
+ model.registerFlow('I1', {
69
+ on: { eventName: 'go' },
70
+ sort: 0,
71
+ steps: { i1: { handler: async () => (calls.push('inst1'), 'i1-ok') } as any },
72
+ });
73
+ model.registerFlow('I2', {
74
+ on: { eventName: 'go' },
75
+ sort: 2,
76
+ steps: { i2: { handler: async () => (calls.push('inst2'), 'i2-ok') } as any },
77
+ });
78
+ model.registerFlow('ERR', {
79
+ on: { eventName: 'go' },
80
+ sort: 3,
81
+ steps: {
82
+ e: {
83
+ handler: async () => {
84
+ calls.push('err');
85
+ throw new Error('boom');
86
+ },
87
+ } as any,
88
+ },
89
+ });
90
+
91
+ const results = await model.dispatchEvent('go', undefined, { sequential: false });
92
+
93
+ // All handlers ran; order is not guaranteed in parallel mode
94
+ expect(new Set(calls)).toEqual(new Set(['inst1', 'inst2', 'err']));
95
+ // The error flow returns undefined and is filtered out; others return stepResults objects
96
+ // Expect one result object per successful flow with its step key
97
+ expect(Array.isArray(results)).toBe(true);
98
+ const keys = (results as any[]).flatMap((o) => Object.keys(o));
99
+ expect(new Set(keys)).toEqual(new Set(['i1', 'i2', 's']));
100
+ const byKey = Object.assign({}, ...(results as any[]));
101
+ expect(byKey['i1']).toBe('i1-ok');
102
+ expect(byKey['i2']).toBe('i2-ok');
103
+ expect(byKey['s']).toBe('static-ok');
104
+ });
105
+
106
+ test('sequential non-beforeRender: stops subsequent flows on error', async () => {
107
+ const engine = new FlowEngine();
108
+ class M extends FlowModel {}
109
+ engine.registerModels({ M });
110
+
111
+ const calls: string[] = [];
112
+ const model = engine.createModel({ use: 'M' });
113
+
114
+ model.registerFlow('I1', {
115
+ on: { eventName: 'go' },
116
+ sort: 0,
117
+ steps: { i1: { handler: async () => void calls.push('i1') } as any },
118
+ });
119
+ model.registerFlow('ERR', {
120
+ on: { eventName: 'go' },
121
+ sort: 1,
122
+ steps: {
123
+ e: {
124
+ handler: async () => {
125
+ throw new Error('X');
126
+ },
127
+ } as any,
128
+ },
129
+ });
130
+ model.registerFlow('I2', {
131
+ on: { eventName: 'go' },
132
+ sort: 2,
133
+ steps: { i2: { handler: async () => void calls.push('i2') } as any },
134
+ });
135
+
136
+ await model.dispatchEvent('go', undefined, { sequential: true }).catch(() => undefined);
137
+ // 只应执行第一个 flow,后续被中止
138
+ expect(calls).toEqual(['i1']);
139
+ });
140
+
141
+ test('beforeRender errors are thrown (sequential default)', async () => {
142
+ const engine = new FlowEngine();
143
+ class M extends FlowModel {}
144
+ engine.registerModels({ M });
145
+
146
+ const calls: string[] = [];
147
+ const model = engine.createModel({ use: 'M' });
148
+
149
+ model.registerFlow('F1', {
150
+ on: 'beforeRender',
151
+ sort: 0,
152
+ steps: {
153
+ a: {
154
+ handler: async () => {
155
+ throw new Error('BR');
156
+ },
157
+ } as any,
158
+ },
159
+ });
160
+ model.registerFlow('F2', {
161
+ on: 'beforeRender',
162
+ sort: 1,
163
+ steps: { b: { handler: async () => void calls.push('b') } as any },
164
+ });
165
+
166
+ await expect(model.dispatchEvent('beforeRender', undefined, { useCache: false })).rejects.toThrow('BR');
167
+ expect(calls).toEqual([]);
168
+ });
169
+ });
@@ -22,7 +22,7 @@ describe('FlowModel.getFlows sorting and getEventFlows(beforeRender) order', ()
22
22
  model = new TestFlowModel({ flowEngine: fakeEngine } as any);
23
23
  });
24
24
 
25
- test('getFlows returns Map ordered by sort ascending', () => {
25
+ test('getFlows returns dynamic(instance) flows first, each group ordered by sort ascending', () => {
26
26
  // class-level (static) flows
27
27
  TestFlowModel.registerFlow('flowA', { title: 'A', sort: 10, steps: {} });
28
28
  TestFlowModel.registerFlow('flowB', { title: 'B', sort: 5, steps: {} });
@@ -34,7 +34,8 @@ describe('FlowModel.getFlows sorting and getEventFlows(beforeRender) order', ()
34
34
  const flows = model.getFlows();
35
35
  const orderedKeys = Array.from(flows.keys());
36
36
 
37
- expect(orderedKeys).toEqual(['flowD', 'flowB', 'flowC', 'flowA']);
37
+ // 动态流组:flowD(0), flowC(7);静态流组:flowB(5), flowA(10)
38
+ expect(orderedKeys).toEqual(['flowD', 'flowC', 'flowB', 'flowA']);
38
39
  });
39
40
 
40
41
  test("getEventFlows('beforeRender') keeps getFlows order and filters out manual/on flows", () => {
@@ -54,21 +55,22 @@ describe('FlowModel.getFlows sorting and getEventFlows(beforeRender) order', ()
54
55
  const autoFlowKeys = model.getEventFlows('beforeRender').map((f) => f.key);
55
56
 
56
57
  // auto flows should exclude event/manual flows
57
- expect(autoFlowKeys).toEqual(['flowD', 'flowB', 'flowC', 'flowE', 'flowA']);
58
+ // 新顺序:动态流组(flowD 0, flowC 7, flowE 8)→ 静态流组(flowB 5, flowA 10)
59
+ expect(autoFlowKeys).toEqual(['flowD', 'flowC', 'flowE', 'flowB', 'flowA']);
58
60
 
59
61
  // relative order should match getFlows order (subset in same sequence)
60
62
  const filteredGetFlowsOrder = getFlowsOrder.filter((k) => !['eventFlow', 'manualFlow'].includes(k));
61
63
  expect(autoFlowKeys).toEqual(filteredGetFlowsOrder);
62
64
  });
63
65
 
64
- test('getFlows tie-breaker: static before instance when sort equal', () => {
66
+ test('getFlows tie-breaker: instance before static when sort equal', () => {
65
67
  // static flow with sort 2
66
68
  TestFlowModel.registerFlow('static2', { title: 'S2', sort: 2, steps: {} });
67
69
  // instance flow with same sort 2
68
70
  model.registerFlow('instance2', { title: 'I2', sort: 2, steps: {} });
69
71
 
70
72
  const keys = Array.from(model.getFlows().keys());
71
- expect(keys.indexOf('static2')).toBeLessThan(keys.indexOf('instance2'));
73
+ expect(keys.indexOf('instance2')).toBeLessThan(keys.indexOf('static2'));
72
74
  });
73
75
 
74
76
  test('getFlows tie-breaker: parent static before child static when sort equal', () => {
@@ -97,4 +99,26 @@ describe('FlowModel.getFlows sorting and getEventFlows(beforeRender) order', ()
97
99
  const keys = Array.from(staticFlows.keys());
98
100
  expect(keys.indexOf('p')).toBeLessThan(keys.indexOf('c'));
99
101
  });
102
+
103
+ test('instance flows keep registration order when sort equal', () => {
104
+ // same sort for all instance flows -> keep registration order
105
+ model.registerFlow('i1', { title: 'I1', sort: 1, steps: {} } as any);
106
+ model.registerFlow('i2', { title: 'I2', sort: 1, steps: {} } as any);
107
+ model.registerFlow('i3', { title: 'I3', sort: 1, steps: {} } as any);
108
+
109
+ // add some static flows to ensure grouping doesn't affect instance intra-order
110
+ (TestFlowModel as any).registerFlow('s1', { title: 'S1', sort: 1, steps: {} });
111
+ (TestFlowModel as any).registerFlow('s2', { title: 'S2', sort: 2, steps: {} });
112
+
113
+ const keys = Array.from(model.getFlows().keys());
114
+ // Instance group appears first and preserves registration order
115
+ const posI1 = keys.indexOf('i1');
116
+ const posI2 = keys.indexOf('i2');
117
+ const posI3 = keys.indexOf('i3');
118
+ expect(posI1).toBeLessThan(posI2);
119
+ expect(posI2).toBeLessThan(posI3);
120
+
121
+ // Static group follows
122
+ expect(keys.slice(posI3 + 1)).toEqual(expect.arrayContaining(['s1', 's2']));
123
+ });
100
124
  });
@@ -898,7 +898,7 @@ describe('FlowModel', () => {
898
898
  expect(_dispatchEventSpy).toHaveBeenCalledWith(
899
899
  'normalEvent',
900
900
  { data: 'test' },
901
- expect.objectContaining({ sequential: undefined }),
901
+ expect.objectContaining({ sequential: true }),
902
902
  );
903
903
  expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
904
904
 
@@ -919,7 +919,7 @@ describe('FlowModel', () => {
919
919
  expect(_dispatchEventSpy).toHaveBeenCalledWith(
920
920
  'defaultEvent',
921
921
  { data: 'test' },
922
- expect.objectContaining({ sequential: undefined }),
922
+ expect.objectContaining({ sequential: true }),
923
923
  );
924
924
  expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
925
925
 
@@ -940,7 +940,7 @@ describe('FlowModel', () => {
940
940
  expect(_dispatchEventSpy).toHaveBeenCalledWith(
941
941
  'undefinedOptionsEvent',
942
942
  { data: 'test' },
943
- expect.objectContaining({ sequential: undefined }),
943
+ expect.objectContaining({ sequential: true }),
944
944
  );
945
945
  expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
946
946
 
@@ -611,17 +611,22 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
611
611
  const instanceKeys = new Set(instanceFlows.keys());
612
612
  const staticEntries = Array.from(staticFlows.entries()).filter(([key]) => !instanceKeys.has(key));
613
613
 
614
- // 实例流保持原始注册顺序,统一一次排序(稳定排序):
614
+ // 组内排序:
615
+ // - 静态流:GlobalFlowRegistry 已按 sort 和继承深度排序,直接使用
616
+ // - 实例流:按 sort 升序;相同 sort 保持注册顺序
615
617
  const instanceEntries = Array.from(instanceFlows.entries());
616
- const allEntries = [...staticEntries, ...instanceEntries];
617
- allEntries.sort(([, a], [, b]) => {
618
- const sa = a.sort ?? 0;
619
- const sb = b.sort ?? 0;
618
+ const instanceEntriesWithIndex = instanceEntries.map((e, i) => ({ e, i }));
619
+ instanceEntriesWithIndex.sort((a, b) => {
620
+ const sa = a.e[1].sort ?? 0;
621
+ const sb = b.e[1].sort ?? 0;
620
622
  if (sa !== sb) return sa - sb;
621
- return 0; // 其它情况保持稳定顺序(静态内部:父类优先;实例内部:注册顺序)
623
+ return a.i - b.i; // 稳定顺序
622
624
  });
623
625
 
624
- return new Map<string, FlowDefinition>(allEntries);
626
+ // 分组合并:动态流(实例)优先于静态流
627
+ const merged: [string, FlowDefinition][] = [...instanceEntriesWithIndex.map(({ e }) => e), ...staticEntries];
628
+
629
+ return new Map<string, FlowDefinition>(merged);
625
630
  }
626
631
 
627
632
  setProps(props: IModelComponentProps): void;
@@ -733,8 +738,8 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
733
738
  } & DispatchEventOptions,
734
739
  ): Promise<any[]> {
735
740
  const isBeforeRender = eventName === 'beforeRender';
736
- // 缺省值由模型层提供:beforeRender 默认顺序执行 + 使用缓存;可被 options 覆盖
737
- const defaults = isBeforeRender ? { sequential: true, useCache: true } : {};
741
+ // 缺省值由模型层提供:beforeRender 默认顺序执行 + 使用缓存;其它事件默认顺序执行(不默认使用缓存)
742
+ const defaults = isBeforeRender ? { sequential: true, useCache: true } : { sequential: true };
738
743
  const execOptions = {
739
744
  sequential: options?.sequential ?? (defaults as any).sequential,
740
745
  useCache: options?.useCache ?? (defaults as any).useCache,
@@ -0,0 +1,65 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, test, expect } from 'vitest';
11
+ import { FlowEngine } from '../../flowEngine';
12
+ import { FlowModel } from '../../models/flowModel';
13
+ import { isBeforeRenderFlow } from '../index';
14
+
15
+ describe('utils/isBeforeRenderFlow', () => {
16
+ test('identifies beforeRender by on string', () => {
17
+ const engine = new FlowEngine();
18
+ class M extends FlowModel {}
19
+ engine.registerModels({ M });
20
+ const m = engine.createModel({ use: 'M' });
21
+ m.registerFlow('A', { on: 'beforeRender', steps: {} });
22
+ const flow = m.flowRegistry.getFlow('A')!;
23
+ expect(isBeforeRenderFlow(flow)).toBe(true);
24
+ });
25
+
26
+ test('identifies beforeRender by on object', () => {
27
+ const engine = new FlowEngine();
28
+ class M extends FlowModel {}
29
+ engine.registerModels({ M });
30
+ const m = engine.createModel({ use: 'M' });
31
+ m.registerFlow('B', { on: { eventName: 'beforeRender' }, steps: {} });
32
+ const flow = m.flowRegistry.getFlow('B')!;
33
+ expect(isBeforeRenderFlow(flow)).toBe(true);
34
+ });
35
+
36
+ test('treats missing on and non-manual as beforeRender', () => {
37
+ const engine = new FlowEngine();
38
+ class M extends FlowModel {}
39
+ engine.registerModels({ M });
40
+ const m = engine.createModel({ use: 'M' });
41
+ m.registerFlow('C', { steps: {} });
42
+ const flow = m.flowRegistry.getFlow('C')!;
43
+ expect(isBeforeRenderFlow(flow)).toBe(true);
44
+ });
45
+
46
+ test('manual flow is not beforeRender when on is missing', () => {
47
+ const engine = new FlowEngine();
48
+ class M extends FlowModel {}
49
+ engine.registerModels({ M });
50
+ const m = engine.createModel({ use: 'M' });
51
+ m.registerFlow('D', { manual: true, steps: {} });
52
+ const flow = m.flowRegistry.getFlow('D')!;
53
+ expect(isBeforeRenderFlow(flow)).toBe(false);
54
+ });
55
+
56
+ test('non-beforeRender event is not beforeRender', () => {
57
+ const engine = new FlowEngine();
58
+ class M extends FlowModel {}
59
+ engine.registerModels({ M });
60
+ const m = engine.createModel({ use: 'M' });
61
+ m.registerFlow('E', { on: { eventName: 'click' }, steps: {} });
62
+ const flow = m.flowRegistry.getFlow('E')!;
63
+ expect(isBeforeRenderFlow(flow)).toBe(false);
64
+ });
65
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { FlowDefinition } from '../FlowDefinition';
11
+
12
+ export const isBeforeRenderFlow = (flow: FlowDefinition): boolean => {
13
+ if (typeof flow.on === 'string') {
14
+ return flow.on === 'beforeRender';
15
+ }
16
+ if (typeof flow.on === 'object') {
17
+ return flow.on.eventName === 'beforeRender';
18
+ }
19
+ if (!flow.on && flow.manual !== true) {
20
+ return true;
21
+ }
22
+ return false;
23
+ };
@@ -63,5 +63,9 @@ export { buildSettingsViewInputArgs } from './buildSettingsViewInputArgs';
63
63
  // 安全全局对象(window/document)
64
64
  export { createSafeDocument, createSafeWindow, createSafeNavigator } from './safeGlobals';
65
65
 
66
+ // Ephemeral context helper(用于临时注入属性/方法,避免污染父级 ctx)
67
+ export { createEphemeralContext } from './createEphemeralContext';
68
+
66
69
  // Filter helpers
67
70
  export { pruneFilter } from './pruneFilter';
71
+ export { isBeforeRenderFlow } from './flows';