@nocobase/flow-engine 2.1.0-alpha.13 → 2.1.0-alpha.15
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.
- package/lib/components/FlowModelRenderer.d.ts +1 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +48 -9
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +19 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +332 -296
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +272 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +247 -0
- package/lib/data-source/index.js +6 -0
- package/lib/flowContext.js +27 -0
- package/lib/flowEngine.d.ts +15 -3
- package/lib/flowEngine.js +62 -6
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +7 -7
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +1 -1
- package/lib/types.d.ts +15 -16
- package/package.json +5 -4
- package/src/__tests__/flow-engine.test.ts +154 -36
- package/src/__tests__/flowContext.test.ts +65 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +1 -5
- package/src/__tests__/flowEngine.saveModel.test.ts +0 -4
- package/src/__tests__/runjsSnippets.test.ts +2 -2
- package/src/components/FlowModelRenderer.tsx +3 -1
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +17 -7
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +63 -9
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +457 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +95 -0
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +547 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +358 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +281 -0
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +0 -1
- package/src/data-source/index.ts +6 -0
- package/src/flowContext.ts +30 -0
- package/src/flowEngine.ts +79 -11
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +7 -7
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +1 -1
- package/src/types.ts +17 -16
package/lib/flowEngine.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { FlowResource } from './resources';
|
|
|
8
8
|
import { Emitter } from './emitter';
|
|
9
9
|
import ModelOperationScheduler from './scheduler/ModelOperationScheduler';
|
|
10
10
|
import type { ScheduleOptions, ScheduledCancel } from './scheduler/ModelOperationScheduler';
|
|
11
|
-
import type { ActionDefinition, ApplyFlowCacheEntry, CreateModelOptions, EnsureBatchResult, EventDefinition,
|
|
11
|
+
import type { ActionDefinition, ApplyFlowCacheEntry, CreateModelOptions, EnsureBatchResult, EventDefinition, FlowModelLoaderInputMap, FlowModelOptions, IFlowModelRepository, ModelConstructor, PersistOptions, ResourceType } from './types';
|
|
12
12
|
/**
|
|
13
13
|
* FlowEngine is the core class of the flow engine, responsible for managing flow models, actions, model repository, and more.
|
|
14
14
|
* It provides capabilities for registering, creating, finding, persisting, replacing, and moving models.
|
|
@@ -262,16 +262,19 @@ export declare class FlowEngine {
|
|
|
262
262
|
registerModels(models: Record<string, ModelConstructor | typeof FlowModel<any>>): void;
|
|
263
263
|
/**
|
|
264
264
|
* Register multiple model loader entries.
|
|
265
|
-
*
|
|
265
|
+
* The `extends` field declares parent class(es) for async subclass discovery via `getSubclassesOfAsync`.
|
|
266
|
+
* It accepts `string | ModelConstructor | (string | ModelConstructor)[]` and is normalized to `string[]` internally.
|
|
267
|
+
* @param {FlowModelLoaderInputMap} loaders Model loader input map
|
|
266
268
|
* @returns {void}
|
|
267
269
|
* @example
|
|
268
270
|
* flowEngine.registerModelLoaders({
|
|
269
271
|
* DemoModel: {
|
|
272
|
+
* extends: 'BaseModel',
|
|
270
273
|
* loader: () => import('./models/DemoModel'),
|
|
271
274
|
* },
|
|
272
275
|
* });
|
|
273
276
|
*/
|
|
274
|
-
registerModelLoaders(loaders:
|
|
277
|
+
registerModelLoaders(loaders: FlowModelLoaderInputMap): void;
|
|
275
278
|
/**
|
|
276
279
|
* Get a registered model class (constructor) asynchronously.
|
|
277
280
|
* This will first ensure the model loader entry is resolved.
|
|
@@ -381,6 +384,15 @@ export declare class FlowEngine {
|
|
|
381
384
|
* @returns {Map<string, ModelConstructor>} Model classes inherited from base class and passed the filter
|
|
382
385
|
*/
|
|
383
386
|
getSubclassesOf(baseClass: string | ModelConstructor, filter?: (ModelClass: ModelConstructor, className: string) => boolean): Map<string, ModelConstructor>;
|
|
387
|
+
/**
|
|
388
|
+
* Asynchronously get all subclasses of a base class, including those registered via model loaders.
|
|
389
|
+
* Merges results from already-loaded classes (_modelClasses) and async loader entries with matching `extends` declarations.
|
|
390
|
+
* Loader-resolved classes are validated with `isInheritedFrom`; mismatches are warned and excluded.
|
|
391
|
+
* @param {string | ModelConstructor} baseClass Base class name or constructor
|
|
392
|
+
* @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function
|
|
393
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model classes that are subclasses of the base class
|
|
394
|
+
*/
|
|
395
|
+
getSubclassesOfAsync(baseClass: string | ModelConstructor, filter?: (ModelClass: ModelConstructor, className: string) => boolean): Promise<Map<string, ModelConstructor>>;
|
|
384
396
|
/**
|
|
385
397
|
* Create and register a model instance.
|
|
386
398
|
* If an instance with the same UID exists, returns the existing instance.
|
package/lib/flowEngine.js
CHANGED
|
@@ -418,21 +418,31 @@ const _FlowEngine = class _FlowEngine {
|
|
|
418
418
|
}
|
|
419
419
|
/**
|
|
420
420
|
* Register multiple model loader entries.
|
|
421
|
-
*
|
|
421
|
+
* The `extends` field declares parent class(es) for async subclass discovery via `getSubclassesOfAsync`.
|
|
422
|
+
* It accepts `string | ModelConstructor | (string | ModelConstructor)[]` and is normalized to `string[]` internally.
|
|
423
|
+
* @param {FlowModelLoaderInputMap} loaders Model loader input map
|
|
422
424
|
* @returns {void}
|
|
423
425
|
* @example
|
|
424
426
|
* flowEngine.registerModelLoaders({
|
|
425
427
|
* DemoModel: {
|
|
428
|
+
* extends: 'BaseModel',
|
|
426
429
|
* loader: () => import('./models/DemoModel'),
|
|
427
430
|
* },
|
|
428
431
|
* });
|
|
429
432
|
*/
|
|
430
433
|
registerModelLoaders(loaders) {
|
|
431
434
|
let changed = false;
|
|
432
|
-
for (const [name,
|
|
435
|
+
for (const [name, input] of Object.entries(loaders)) {
|
|
433
436
|
if (this._modelLoaders.has(name)) {
|
|
434
437
|
console.warn(`FlowEngine: Model loader with name '${name}' is already registered and will be overwritten.`);
|
|
435
438
|
}
|
|
439
|
+
const entry = {
|
|
440
|
+
loader: input.loader
|
|
441
|
+
};
|
|
442
|
+
if (input.extends != null) {
|
|
443
|
+
const raw = Array.isArray(input.extends) ? input.extends : [input.extends];
|
|
444
|
+
entry.extends = raw.map((item) => typeof item === "string" ? item : item.name);
|
|
445
|
+
}
|
|
436
446
|
this._modelLoaders.set(name, entry);
|
|
437
447
|
changed = true;
|
|
438
448
|
}
|
|
@@ -737,6 +747,55 @@ const _FlowEngine = class _FlowEngine {
|
|
|
737
747
|
}
|
|
738
748
|
return result;
|
|
739
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Asynchronously get all subclasses of a base class, including those registered via model loaders.
|
|
752
|
+
* Merges results from already-loaded classes (_modelClasses) and async loader entries with matching `extends` declarations.
|
|
753
|
+
* Loader-resolved classes are validated with `isInheritedFrom`; mismatches are warned and excluded.
|
|
754
|
+
* @param {string | ModelConstructor} baseClass Base class name or constructor
|
|
755
|
+
* @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function
|
|
756
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model classes that are subclasses of the base class
|
|
757
|
+
*/
|
|
758
|
+
async getSubclassesOfAsync(baseClass, filter) {
|
|
759
|
+
var _a;
|
|
760
|
+
const baseClassName = typeof baseClass === "string" ? baseClass : baseClass.name;
|
|
761
|
+
let parentModelClass;
|
|
762
|
+
if (typeof baseClass === "string") {
|
|
763
|
+
if (!this.getModelClass(baseClass)) {
|
|
764
|
+
await this.ensureModel(baseClass);
|
|
765
|
+
}
|
|
766
|
+
parentModelClass = this.getModelClass(baseClass);
|
|
767
|
+
} else {
|
|
768
|
+
parentModelClass = baseClass;
|
|
769
|
+
}
|
|
770
|
+
if (!parentModelClass) {
|
|
771
|
+
return /* @__PURE__ */ new Map();
|
|
772
|
+
}
|
|
773
|
+
const result = this.getSubclassesOf(parentModelClass, filter);
|
|
774
|
+
const loaderCandidates = [];
|
|
775
|
+
for (const [name, entry] of this._modelLoaders) {
|
|
776
|
+
if (result.has(name) || this._modelClasses.has(name)) continue;
|
|
777
|
+
if ((_a = entry.extends) == null ? void 0 : _a.includes(baseClassName)) {
|
|
778
|
+
loaderCandidates.push(name);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (loaderCandidates.length > 0) {
|
|
782
|
+
await this.ensureModels(loaderCandidates);
|
|
783
|
+
}
|
|
784
|
+
for (const name of loaderCandidates) {
|
|
785
|
+
const ModelClass = this._modelClasses.get(name);
|
|
786
|
+
if (!ModelClass) continue;
|
|
787
|
+
if (!(0, import_utils.isInheritedFrom)(ModelClass, parentModelClass)) {
|
|
788
|
+
console.warn(
|
|
789
|
+
`FlowEngine: Model '${name}' declares extends '${baseClassName}' but does not actually inherit from it. Skipping.`
|
|
790
|
+
);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (!filter || filter(ModelClass, name)) {
|
|
794
|
+
result.set(name, ModelClass);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
740
799
|
/**
|
|
741
800
|
* Create and register a model instance.
|
|
742
801
|
* If an instance with the same UID exists, returns the existing instance.
|
|
@@ -1140,10 +1199,7 @@ const _FlowEngine = class _FlowEngine {
|
|
|
1140
1199
|
if (hydrated) {
|
|
1141
1200
|
return hydrated;
|
|
1142
1201
|
}
|
|
1143
|
-
const
|
|
1144
|
-
const data = shouldUseEnsure ? await this._modelRepository.ensure(options, {
|
|
1145
|
-
includeAsyncNode: !!((options == null ? void 0 : options.includeAsyncNode) || (options == null ? void 0 : options.async))
|
|
1146
|
-
}) ?? await this._modelRepository.findOne(options) : await this._modelRepository.findOne(options);
|
|
1202
|
+
const data = await this._modelRepository.findOne(options);
|
|
1147
1203
|
let model = null;
|
|
1148
1204
|
if (data == null ? void 0 : data.uid) {
|
|
1149
1205
|
model = await this.createModelAsync(data, extra);
|
package/lib/locale/en-US.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"Failed to destroy model after creation error": "Failed to destroy model after creation error",
|
|
35
35
|
"Failed to get action {{action}}": "Failed to get action {{action}}",
|
|
36
36
|
"Failed to get configurable flows for model {{model}}": "Failed to get configurable flows for model {{model}}",
|
|
37
|
+
"Attributes are unavailable before selecting a record": "Attributes are unavailable before selecting a record",
|
|
37
38
|
"Failed to import FormDialog": "Failed to import FormDialog",
|
|
38
39
|
"Failed to import FormDialog or FormStep": "Failed to import FormDialog or FormStep",
|
|
39
40
|
"Failed to import Formily components": "Failed to import Formily components",
|
package/lib/locale/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export declare const locales: {
|
|
|
43
43
|
"Failed to destroy model after creation error": string;
|
|
44
44
|
"Failed to get action {{action}}": string;
|
|
45
45
|
"Failed to get configurable flows for model {{model}}": string;
|
|
46
|
+
"Attributes are unavailable before selecting a record": string;
|
|
46
47
|
"Failed to import FormDialog": string;
|
|
47
48
|
"Failed to import FormDialog or FormStep": string;
|
|
48
49
|
"Failed to import Formily components": string;
|
|
@@ -120,6 +121,7 @@ export declare const locales: {
|
|
|
120
121
|
"Failed to destroy model after creation error": string;
|
|
121
122
|
"Failed to get action {{action}}": string;
|
|
122
123
|
"Failed to get configurable flows for model {{model}}": string;
|
|
124
|
+
"Attributes are unavailable before selecting a record": string;
|
|
123
125
|
"Failed to import FormDialog": string;
|
|
124
126
|
"Failed to import FormDialog or FormStep": string;
|
|
125
127
|
"Failed to import Formily components": string;
|
package/lib/locale/zh-CN.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"Failed to destroy model after creation error": "创建错误后销毁模型失败",
|
|
32
32
|
"Failed to get action {{action}}": "获取 action '{{action}}' 失败",
|
|
33
33
|
"Failed to get configurable flows for model {{model}}": "获取模型 '{{model}}' 的可配置 flows 失败",
|
|
34
|
+
"Attributes are unavailable before selecting a record": "选择记录之前,当前项属性不可用",
|
|
34
35
|
"Failed to import FormDialog": "导入 FormDialog 失败",
|
|
35
36
|
"Failed to import FormDialog or FormStep": "导入 FormDialog 或 FormStep 失败",
|
|
36
37
|
"Failed to import Formily components": "导入 Formily 组件失败",
|
package/lib/reactive/observer.js
CHANGED
|
@@ -51,8 +51,31 @@ const observer = /* @__PURE__ */ __name((Component, options) => {
|
|
|
51
51
|
const ctxRef = (0, import_react.useRef)(ctx);
|
|
52
52
|
ctxRef.current = ctx;
|
|
53
53
|
const pendingDisposerRef = (0, import_react.useRef)(null);
|
|
54
|
+
const pendingTimerRef = (0, import_react.useRef)(null);
|
|
55
|
+
const clearPendingDisposer = /* @__PURE__ */ __name(() => {
|
|
56
|
+
if (pendingDisposerRef.current) {
|
|
57
|
+
pendingDisposerRef.current();
|
|
58
|
+
pendingDisposerRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
}, "clearPendingDisposer");
|
|
61
|
+
const clearPendingTimer = /* @__PURE__ */ __name(() => {
|
|
62
|
+
if (pendingTimerRef.current) {
|
|
63
|
+
clearTimeout(pendingTimerRef.current);
|
|
64
|
+
pendingTimerRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
}, "clearPendingTimer");
|
|
67
|
+
const isContextActive = /* @__PURE__ */ __name(() => {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
const pageActive = getPageActive(ctxRef.current);
|
|
70
|
+
const tabActive = (_b = (_a = ctxRef.current) == null ? void 0 : _a.tabActive) == null ? void 0 : _b.value;
|
|
71
|
+
return pageActive !== false && tabActive !== false;
|
|
72
|
+
}, "isContextActive");
|
|
54
73
|
(0, import_react.useEffect)(() => {
|
|
55
74
|
return () => {
|
|
75
|
+
if (pendingTimerRef.current) {
|
|
76
|
+
clearTimeout(pendingTimerRef.current);
|
|
77
|
+
pendingTimerRef.current = null;
|
|
78
|
+
}
|
|
56
79
|
if (pendingDisposerRef.current) {
|
|
57
80
|
pendingDisposerRef.current();
|
|
58
81
|
pendingDisposerRef.current = null;
|
|
@@ -62,30 +85,37 @@ const observer = /* @__PURE__ */ __name((Component, options) => {
|
|
|
62
85
|
const ObservedComponent = (0, import_react.useMemo)(
|
|
63
86
|
() => (0, import_reactive_react.observer)(Component, {
|
|
64
87
|
scheduler(updater) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
setTimeout(() => {
|
|
88
|
+
if (!isContextActive()) {
|
|
89
|
+
if (pendingTimerRef.current || pendingDisposerRef.current) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
pendingTimerRef.current = setTimeout(() => {
|
|
93
|
+
pendingTimerRef.current = null;
|
|
70
94
|
if (pendingDisposerRef.current) {
|
|
71
95
|
return;
|
|
72
96
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
if (isContextActive()) {
|
|
98
|
+
updater();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
pendingDisposerRef.current = (0, import_reactive.reaction)(
|
|
102
|
+
() => isContextActive(),
|
|
103
|
+
(active) => {
|
|
104
|
+
if (!active) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
clearPendingDisposer();
|
|
76
108
|
updater();
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "FlowObserverPendingUpdate"
|
|
79
112
|
}
|
|
80
|
-
|
|
81
|
-
pendingDisposerRef.current = disposer;
|
|
113
|
+
);
|
|
82
114
|
});
|
|
83
115
|
return;
|
|
84
116
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
pendingDisposerRef.current = null;
|
|
88
|
-
}
|
|
117
|
+
clearPendingTimer();
|
|
118
|
+
clearPendingDisposer();
|
|
89
119
|
updater();
|
|
90
120
|
},
|
|
91
121
|
...options
|
|
@@ -32,18 +32,18 @@ module.exports = __toCommonJS(set_field_style_snippet_exports);
|
|
|
32
32
|
const snippet = {
|
|
33
33
|
contexts: ["*"],
|
|
34
34
|
scenes: ["detailFieldEvent", "formFieldEvent"],
|
|
35
|
-
prefix: "sn-
|
|
36
|
-
label: "Set
|
|
37
|
-
description: "Customize form and
|
|
35
|
+
prefix: "sn-item-style",
|
|
36
|
+
label: "Set form item/details item style",
|
|
37
|
+
description: "Customize form item and details item container styles",
|
|
38
38
|
locales: {
|
|
39
39
|
"zh-CN": {
|
|
40
|
-
label: "\u8868\u5355\
|
|
41
|
-
description: "\u81EA\u5B9A\u4E49\u8868\u5355\
|
|
40
|
+
label: "\u8BBE\u7F6E\u8868\u5355\u9879/\u8BE6\u60C5\u9879\u6837\u5F0F",
|
|
41
|
+
description: "\u81EA\u5B9A\u4E49\u8868\u5355\u9879\u548C\u8BE6\u60C5\u9879\u5BB9\u5668\u6837\u5F0F"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
content: `
|
|
45
|
-
ctx.model.
|
|
46
|
-
|
|
45
|
+
ctx.model.props.style = {
|
|
46
|
+
background: 'red',
|
|
47
47
|
};
|
|
48
48
|
`
|
|
49
49
|
};
|
package/lib/types.d.ts
CHANGED
|
@@ -316,17 +316,28 @@ export type FlowModelLoaderResult = ModelConstructor | {
|
|
|
316
316
|
*/
|
|
317
317
|
export type FlowModelLoader = () => Promise<FlowModelLoaderResult>;
|
|
318
318
|
/**
|
|
319
|
-
* FlowModel loader entry.
|
|
320
|
-
* Future contribution-style fields are intentionally kept as commented placeholders in phase A
|
|
321
|
-
* and are not consumed by current runtime logic.
|
|
319
|
+
* FlowModel loader entry (normalized internal form).
|
|
322
320
|
*/
|
|
323
321
|
export interface FlowModelLoaderEntry {
|
|
324
322
|
loader: FlowModelLoader;
|
|
323
|
+
extends?: string[];
|
|
325
324
|
}
|
|
326
325
|
/**
|
|
327
|
-
* FlowModel loader
|
|
326
|
+
* FlowModel loader input (user-facing form for registerModelLoaders).
|
|
327
|
+
* The `extends` field accepts flexible formats that will be normalized to `string[]` at registration time.
|
|
328
|
+
*/
|
|
329
|
+
export interface FlowModelLoaderInput {
|
|
330
|
+
loader: FlowModelLoader;
|
|
331
|
+
extends?: string | ModelConstructor | (string | ModelConstructor)[];
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* FlowModel loader entry map (normalized internal form).
|
|
328
335
|
*/
|
|
329
336
|
export type FlowModelLoaderMap = Record<string, FlowModelLoaderEntry>;
|
|
337
|
+
/**
|
|
338
|
+
* FlowModel loader input map (user-facing form for registerModelLoaders).
|
|
339
|
+
*/
|
|
340
|
+
export type FlowModelLoaderInputMap = Record<string, FlowModelLoaderInput>;
|
|
330
341
|
/**
|
|
331
342
|
* Batch ensure result.
|
|
332
343
|
*/
|
|
@@ -340,18 +351,6 @@ export interface EnsureBatchResult {
|
|
|
340
351
|
}
|
|
341
352
|
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
|
|
342
353
|
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
|
|
343
|
-
/**
|
|
344
|
-
* Ensure a model exists (create if missing) in a single request.
|
|
345
|
-
*/
|
|
346
|
-
ensure: (values: Record<string, any>, options?: {
|
|
347
|
-
includeAsyncNode?: boolean;
|
|
348
|
-
}) => Promise<Record<string, any> | null>;
|
|
349
|
-
/**
|
|
350
|
-
* Optional: run multiple ops in a single transaction (server capability).
|
|
351
|
-
*/
|
|
352
|
-
mutate?: (values: Record<string, any>, options?: {
|
|
353
|
-
includeAsyncNode?: boolean;
|
|
354
|
-
}) => Promise<Record<string, any>>;
|
|
355
354
|
save(model: T, options?: {
|
|
356
355
|
onlyStepParams?: boolean;
|
|
357
356
|
}): Promise<Record<string, any>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.15",
|
|
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,9 +8,10 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.1.0-alpha.
|
|
12
|
-
"@nocobase/shared": "2.1.0-alpha.
|
|
11
|
+
"@nocobase/sdk": "2.1.0-alpha.15",
|
|
12
|
+
"@nocobase/shared": "2.1.0-alpha.15",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
|
+
"axios": "^1.7.0",
|
|
14
15
|
"dayjs": "^1.11.9",
|
|
15
16
|
"dompurify": "^3.0.2",
|
|
16
17
|
"lodash": "^4.x",
|
|
@@ -36,5 +37,5 @@
|
|
|
36
37
|
],
|
|
37
38
|
"author": "NocoBase Team",
|
|
38
39
|
"license": "Apache-2.0",
|
|
39
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "7c86e75b0af4b9f532c8ebf5ef96a7423b0ab60e"
|
|
40
41
|
}
|
|
@@ -89,13 +89,7 @@ describe('FlowEngine', () => {
|
|
|
89
89
|
class MockFlowModelRepository implements IFlowModelRepository {
|
|
90
90
|
// 使用可配置返回值,便于不同用例控制 findOne 行为
|
|
91
91
|
findOneResult: any = null;
|
|
92
|
-
ensureResult: any = null;
|
|
93
|
-
ensureCalls = 0;
|
|
94
92
|
save = vi.fn(async (model: FlowModel) => ({ success: true, uid: model.uid }));
|
|
95
|
-
async ensure() {
|
|
96
|
-
this.ensureCalls += 1;
|
|
97
|
-
return this.ensureResult ? JSON.parse(JSON.stringify(this.ensureResult)) : null;
|
|
98
|
-
}
|
|
99
93
|
async findOne() {
|
|
100
94
|
// 返回深拷贝,避免被测试过程修改
|
|
101
95
|
return this.findOneResult ? JSON.parse(JSON.stringify(this.findOneResult)) : null;
|
|
@@ -194,47 +188,171 @@ describe('FlowEngine', () => {
|
|
|
194
188
|
expect(Array.isArray(mounted)).toBe(false);
|
|
195
189
|
expect(mounted?.uid).toBe('c3');
|
|
196
190
|
});
|
|
191
|
+
});
|
|
197
192
|
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
describe('getSubclassesOfAsync', () => {
|
|
194
|
+
it('should return async-loaded subclasses matching extends declaration', async () => {
|
|
195
|
+
class AsyncSubModelD extends BaseModel {}
|
|
196
|
+
class AsyncSubModelE extends BaseModel {}
|
|
200
197
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
198
|
+
engine.registerModelLoaders({
|
|
199
|
+
AsyncSubModelD: {
|
|
200
|
+
extends: 'BaseModel',
|
|
201
|
+
loader: async () => ({ AsyncSubModelD }),
|
|
202
|
+
},
|
|
203
|
+
AsyncSubModelE: {
|
|
204
|
+
extends: 'BaseModel',
|
|
205
|
+
loader: async () => ({ AsyncSubModelE }),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
208
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
210
|
+
|
|
211
|
+
// Sync-registered subclasses
|
|
212
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
213
|
+
expect(result.has('SubModelB')).toBe(true);
|
|
214
|
+
expect(result.has('SubModelC')).toBe(true);
|
|
215
|
+
// Async-loaded subclasses
|
|
216
|
+
expect(result.has('AsyncSubModelD')).toBe(true);
|
|
217
|
+
expect(result.has('AsyncSubModelE')).toBe(true);
|
|
218
|
+
// Base class excluded
|
|
219
|
+
expect(result.has('BaseModel')).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should merge sync-registered and async-loaded subclasses', async () => {
|
|
223
|
+
class AsyncSubModel extends BaseModel {}
|
|
224
|
+
|
|
225
|
+
engine.registerModelLoaders({
|
|
226
|
+
AsyncSubModel: {
|
|
227
|
+
extends: 'BaseModel',
|
|
228
|
+
loader: async () => ({ AsyncSubModel }),
|
|
229
|
+
},
|
|
215
230
|
});
|
|
216
231
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
expect(
|
|
232
|
+
const result = await engine.getSubclassesOfAsync('BaseModel');
|
|
233
|
+
|
|
234
|
+
// Sync: SubModelA, SubModelB, SubModelC
|
|
235
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
236
|
+
expect(result.has('SubModelB')).toBe(true);
|
|
237
|
+
expect(result.has('SubModelC')).toBe(true);
|
|
238
|
+
// Async
|
|
239
|
+
expect(result.has('AsyncSubModel')).toBe(true);
|
|
240
|
+
expect(result.size).toBe(4);
|
|
221
241
|
});
|
|
222
242
|
|
|
223
|
-
it('should
|
|
224
|
-
|
|
243
|
+
it('should support extends as string array (multiple parents)', async () => {
|
|
244
|
+
class AnotherBase extends FlowModel {}
|
|
245
|
+
class MultiParentModel extends BaseModel {}
|
|
246
|
+
|
|
247
|
+
engine.registerModels({ AnotherBase });
|
|
248
|
+
engine.registerModelLoaders({
|
|
249
|
+
MultiParentModel: {
|
|
250
|
+
extends: ['BaseModel', 'AnotherBase'],
|
|
251
|
+
loader: async () => ({ MultiParentModel }),
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const resultBase = await engine.getSubclassesOfAsync(BaseModel);
|
|
256
|
+
expect(resultBase.has('MultiParentModel')).toBe(true);
|
|
257
|
+
|
|
258
|
+
// Also found by AnotherBase (even though actual inheritance is from BaseModel, not AnotherBase)
|
|
259
|
+
// The extends declaration triggers loading, but isInheritedFrom validation will exclude it from AnotherBase results
|
|
260
|
+
const resultAnother = await engine.getSubclassesOfAsync(AnotherBase);
|
|
261
|
+
expect(resultAnother.has('MultiParentModel')).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should support extends as ModelConstructor', async () => {
|
|
265
|
+
class AsyncCtorSubModel extends BaseModel {}
|
|
266
|
+
|
|
267
|
+
engine.registerModelLoaders({
|
|
268
|
+
AsyncCtorSubModel: {
|
|
269
|
+
extends: BaseModel,
|
|
270
|
+
loader: async () => ({ AsyncCtorSubModel }),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
275
|
+
expect(result.has('AsyncCtorSubModel')).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should validate actual inheritance and warn on mismatch', async () => {
|
|
279
|
+
class UnrelatedModel extends FlowModel {}
|
|
280
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
225
281
|
|
|
226
|
-
|
|
227
|
-
{
|
|
228
|
-
|
|
229
|
-
|
|
282
|
+
engine.registerModelLoaders({
|
|
283
|
+
UnrelatedModel: {
|
|
284
|
+
extends: 'BaseModel',
|
|
285
|
+
loader: async () => ({ UnrelatedModel }),
|
|
230
286
|
},
|
|
231
|
-
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
290
|
+
expect(result.has('UnrelatedModel')).toBe(false);
|
|
291
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
292
|
+
expect.stringContaining("declares extends 'BaseModel' but does not actually inherit from it"),
|
|
232
293
|
);
|
|
233
294
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
295
|
+
warnSpy.mockRestore();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should resolve base class from loaders if not in _modelClasses', async () => {
|
|
299
|
+
const freshEngine = new FlowEngine();
|
|
300
|
+
|
|
301
|
+
class LazyBase extends FlowModel {}
|
|
302
|
+
class LazySub extends LazyBase {}
|
|
303
|
+
|
|
304
|
+
freshEngine.registerModelLoaders({
|
|
305
|
+
LazyBase: {
|
|
306
|
+
loader: async () => ({ LazyBase }),
|
|
307
|
+
},
|
|
308
|
+
LazySub: {
|
|
309
|
+
extends: 'LazyBase',
|
|
310
|
+
loader: async () => ({ LazySub }),
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = await freshEngine.getSubclassesOfAsync('LazyBase');
|
|
315
|
+
expect(result.has('LazySub')).toBe(true);
|
|
316
|
+
expect(result.size).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should return empty Map when base class cannot be found', async () => {
|
|
320
|
+
const result = await engine.getSubclassesOfAsync('NonExistentModel');
|
|
321
|
+
expect(result.size).toBe(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should support filter parameter on both sync and async sources', async () => {
|
|
325
|
+
class FilteredAsyncModel extends BaseModel {}
|
|
326
|
+
|
|
327
|
+
engine.registerModelLoaders({
|
|
328
|
+
FilteredAsyncModel: {
|
|
329
|
+
extends: 'BaseModel',
|
|
330
|
+
loader: async () => ({ FilteredAsyncModel }),
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const result = await engine.getSubclassesOfAsync(BaseModel, (_ModelClass, name) => name.startsWith('SubModelA'));
|
|
335
|
+
|
|
336
|
+
// Only SubModelA passes the filter (SubModelB, SubModelC, FilteredAsyncModel excluded)
|
|
337
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
338
|
+
expect(result.has('SubModelB')).toBe(false);
|
|
339
|
+
expect(result.has('SubModelC')).toBe(false);
|
|
340
|
+
expect(result.has('FilteredAsyncModel')).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should not include loaders without extends declaration', async () => {
|
|
344
|
+
class NoExtendsModel extends BaseModel {}
|
|
345
|
+
|
|
346
|
+
engine.registerModelLoaders({
|
|
347
|
+
NoExtendsModel: {
|
|
348
|
+
loader: async () => ({ NoExtendsModel }),
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
353
|
+
// Only sync-registered subclasses; NoExtendsModel has no extends, so not discovered
|
|
354
|
+
expect(result.has('NoExtendsModel')).toBe(false);
|
|
355
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
238
356
|
});
|
|
239
357
|
});
|
|
240
358
|
});
|