@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/src/flowEngine.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
EnsureBatchResult,
|
|
28
28
|
EventDefinition,
|
|
29
29
|
FlowModelLoaderEntry,
|
|
30
|
-
|
|
30
|
+
FlowModelLoaderInputMap,
|
|
31
31
|
FlowModelLoaderResult,
|
|
32
32
|
FlowModelOptions,
|
|
33
33
|
IFlowModelRepository,
|
|
@@ -483,21 +483,31 @@ export class FlowEngine {
|
|
|
483
483
|
|
|
484
484
|
/**
|
|
485
485
|
* Register multiple model loader entries.
|
|
486
|
-
*
|
|
486
|
+
* The `extends` field declares parent class(es) for async subclass discovery via `getSubclassesOfAsync`.
|
|
487
|
+
* It accepts `string | ModelConstructor | (string | ModelConstructor)[]` and is normalized to `string[]` internally.
|
|
488
|
+
* @param {FlowModelLoaderInputMap} loaders Model loader input map
|
|
487
489
|
* @returns {void}
|
|
488
490
|
* @example
|
|
489
491
|
* flowEngine.registerModelLoaders({
|
|
490
492
|
* DemoModel: {
|
|
493
|
+
* extends: 'BaseModel',
|
|
491
494
|
* loader: () => import('./models/DemoModel'),
|
|
492
495
|
* },
|
|
493
496
|
* });
|
|
494
497
|
*/
|
|
495
|
-
public registerModelLoaders(loaders:
|
|
498
|
+
public registerModelLoaders(loaders: FlowModelLoaderInputMap): void {
|
|
496
499
|
let changed = false;
|
|
497
|
-
for (const [name,
|
|
500
|
+
for (const [name, input] of Object.entries(loaders)) {
|
|
498
501
|
if (this._modelLoaders.has(name)) {
|
|
499
502
|
console.warn(`FlowEngine: Model loader with name '${name}' is already registered and will be overwritten.`);
|
|
500
503
|
}
|
|
504
|
+
const entry: FlowModelLoaderEntry = {
|
|
505
|
+
loader: input.loader,
|
|
506
|
+
};
|
|
507
|
+
if (input.extends != null) {
|
|
508
|
+
const raw = Array.isArray(input.extends) ? input.extends : [input.extends];
|
|
509
|
+
entry.extends = raw.map((item) => (typeof item === 'string' ? item : item.name));
|
|
510
|
+
}
|
|
501
511
|
this._modelLoaders.set(name, entry);
|
|
502
512
|
changed = true;
|
|
503
513
|
}
|
|
@@ -845,6 +855,70 @@ export class FlowEngine {
|
|
|
845
855
|
return result;
|
|
846
856
|
}
|
|
847
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Asynchronously get all subclasses of a base class, including those registered via model loaders.
|
|
860
|
+
* Merges results from already-loaded classes (_modelClasses) and async loader entries with matching `extends` declarations.
|
|
861
|
+
* Loader-resolved classes are validated with `isInheritedFrom`; mismatches are warned and excluded.
|
|
862
|
+
* @param {string | ModelConstructor} baseClass Base class name or constructor
|
|
863
|
+
* @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function
|
|
864
|
+
* @returns {Promise<Map<string, ModelConstructor>>} Model classes that are subclasses of the base class
|
|
865
|
+
*/
|
|
866
|
+
public async getSubclassesOfAsync(
|
|
867
|
+
baseClass: string | ModelConstructor,
|
|
868
|
+
filter?: (ModelClass: ModelConstructor, className: string) => boolean,
|
|
869
|
+
): Promise<Map<string, ModelConstructor>> {
|
|
870
|
+
const baseClassName = typeof baseClass === 'string' ? baseClass : baseClass.name;
|
|
871
|
+
|
|
872
|
+
// If baseClass is a string and not yet loaded, try to resolve it first
|
|
873
|
+
let parentModelClass: ModelConstructor | undefined;
|
|
874
|
+
if (typeof baseClass === 'string') {
|
|
875
|
+
if (!this.getModelClass(baseClass)) {
|
|
876
|
+
await this.ensureModel(baseClass);
|
|
877
|
+
}
|
|
878
|
+
parentModelClass = this.getModelClass(baseClass);
|
|
879
|
+
} else {
|
|
880
|
+
parentModelClass = baseClass;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!parentModelClass) {
|
|
884
|
+
return new Map();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Step 1: Collect already-loaded subclasses from _modelClasses
|
|
888
|
+
const result = this.getSubclassesOf(parentModelClass, filter);
|
|
889
|
+
|
|
890
|
+
// Step 2: Find unloaded loaders whose extends includes baseClassName
|
|
891
|
+
const loaderCandidates: string[] = [];
|
|
892
|
+
for (const [name, entry] of this._modelLoaders) {
|
|
893
|
+
if (result.has(name) || this._modelClasses.has(name)) continue;
|
|
894
|
+
if (entry.extends?.includes(baseClassName)) {
|
|
895
|
+
loaderCandidates.push(name);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Step 3: Resolve all matching loaders
|
|
900
|
+
if (loaderCandidates.length > 0) {
|
|
901
|
+
await this.ensureModels(loaderCandidates);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Step 4: Validate resolved classes and add to result
|
|
905
|
+
for (const name of loaderCandidates) {
|
|
906
|
+
const ModelClass = this._modelClasses.get(name);
|
|
907
|
+
if (!ModelClass) continue;
|
|
908
|
+
if (!isInheritedFrom(ModelClass, parentModelClass)) {
|
|
909
|
+
console.warn(
|
|
910
|
+
`FlowEngine: Model '${name}' declares extends '${baseClassName}' but does not actually inherit from it. Skipping.`,
|
|
911
|
+
);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (!filter || filter(ModelClass, name)) {
|
|
915
|
+
result.set(name, ModelClass);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
|
|
848
922
|
/**
|
|
849
923
|
* Create and register a model instance.
|
|
850
924
|
* If an instance with the same UID exists, returns the existing instance.
|
|
@@ -1336,13 +1410,7 @@ export class FlowEngine {
|
|
|
1336
1410
|
return hydrated;
|
|
1337
1411
|
}
|
|
1338
1412
|
|
|
1339
|
-
const
|
|
1340
|
-
const data = shouldUseEnsure
|
|
1341
|
-
? (await this._modelRepository.ensure(options, {
|
|
1342
|
-
includeAsyncNode: !!(options?.includeAsyncNode || options?.async),
|
|
1343
|
-
})) ?? (await this._modelRepository.findOne(options))
|
|
1344
|
-
: await this._modelRepository.findOne(options);
|
|
1345
|
-
|
|
1413
|
+
const data = await this._modelRepository.findOne(options);
|
|
1346
1414
|
let model: T | null = null;
|
|
1347
1415
|
if (data?.uid) {
|
|
1348
1416
|
model = await this.createModelAsync<T>(data as any, extra);
|
package/src/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/src/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 组件失败",
|
|
@@ -208,4 +208,86 @@ describe('observer', () => {
|
|
|
208
208
|
expect(screen.getByText('Count: 0')).toBeInTheDocument();
|
|
209
209
|
expect(screen.queryByText('Count: 1')).not.toBeInTheDocument();
|
|
210
210
|
});
|
|
211
|
+
|
|
212
|
+
it('should flush pending update without TDZ error when context becomes active before timer callback runs', async () => {
|
|
213
|
+
vi.useFakeTimers();
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const model = observable({ count: 0 });
|
|
217
|
+
const pageActive = observable.ref(false);
|
|
218
|
+
const tabActive = observable.ref(true);
|
|
219
|
+
|
|
220
|
+
const context = {
|
|
221
|
+
pageActive,
|
|
222
|
+
tabActive,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
(useFlowContext as any).mockReturnValue(context);
|
|
226
|
+
|
|
227
|
+
const Component = observer(() => <div>Count: {model.count}</div>);
|
|
228
|
+
|
|
229
|
+
render(<Component />);
|
|
230
|
+
|
|
231
|
+
expect(screen.getByText('Count: 0')).toBeInTheDocument();
|
|
232
|
+
|
|
233
|
+
act(() => {
|
|
234
|
+
model.count++;
|
|
235
|
+
pageActive.value = true;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await act(async () => {
|
|
239
|
+
await vi.runAllTimersAsync();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(screen.getByText('Count: 1')).toBeInTheDocument();
|
|
243
|
+
} finally {
|
|
244
|
+
vi.useRealTimers();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should cleanup pending timer and listener on unmount', async () => {
|
|
249
|
+
vi.useFakeTimers();
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const model = observable({ count: 0 });
|
|
253
|
+
const pageActive = observable.ref(false);
|
|
254
|
+
const tabActive = observable.ref(true);
|
|
255
|
+
const renderSpy = vi.fn();
|
|
256
|
+
|
|
257
|
+
const context = {
|
|
258
|
+
pageActive,
|
|
259
|
+
tabActive,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
(useFlowContext as any).mockReturnValue(context);
|
|
263
|
+
|
|
264
|
+
const Component = observer(() => {
|
|
265
|
+
renderSpy(model.count);
|
|
266
|
+
return <div>Count: {model.count}</div>;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const { unmount } = render(<Component />);
|
|
270
|
+
|
|
271
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(screen.getByText('Count: 0')).toBeInTheDocument();
|
|
273
|
+
|
|
274
|
+
act(() => {
|
|
275
|
+
model.count++;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
unmount();
|
|
279
|
+
|
|
280
|
+
act(() => {
|
|
281
|
+
pageActive.value = true;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await act(async () => {
|
|
285
|
+
await vi.runAllTimersAsync();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
289
|
+
} finally {
|
|
290
|
+
vi.useRealTimers();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
211
293
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { observer as originalObserver, IObserverOptions, ReactFC } from '@formily/reactive-react';
|
|
11
11
|
import React, { useMemo, useEffect, useRef } from 'react';
|
|
12
12
|
import { useFlowContext } from '../FlowContextProvider';
|
|
13
|
-
import {
|
|
13
|
+
import { reaction } from '@formily/reactive';
|
|
14
14
|
import { FlowEngineContext } from '..';
|
|
15
15
|
|
|
16
16
|
type ObserverComponentProps<P, Options extends IObserverOptions> = Options extends {
|
|
@@ -30,12 +30,67 @@ export const observer = <P, Options extends IObserverOptions = IObserverOptions>
|
|
|
30
30
|
const ctxRef = useRef(ctx);
|
|
31
31
|
ctxRef.current = ctx;
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// 保存延迟更新的监听器,避免重复创建监听。
|
|
34
34
|
const pendingDisposerRef = useRef<(() => void) | null>(null);
|
|
35
|
+
// 保存延迟创建监听器的定时器,避免组件卸载后仍继续调度。
|
|
36
|
+
const pendingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 清理挂起的可见性监听器。
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* clearPendingDisposer();
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
const clearPendingDisposer = () => {
|
|
47
|
+
if (pendingDisposerRef.current) {
|
|
48
|
+
pendingDisposerRef.current();
|
|
49
|
+
pendingDisposerRef.current = null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 清理挂起的定时器。
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* clearPendingTimer();
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
const clearPendingTimer = () => {
|
|
62
|
+
if (pendingTimerRef.current) {
|
|
63
|
+
clearTimeout(pendingTimerRef.current);
|
|
64
|
+
pendingTimerRef.current = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 判断当前页面与标签页是否允许立即更新。
|
|
70
|
+
*
|
|
71
|
+
* @returns 当前上下文是否处于可更新状态。
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* if (isContextActive()) {
|
|
75
|
+
* updater();
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
const isContextActive = () => {
|
|
80
|
+
const pageActive = getPageActive(ctxRef.current);
|
|
81
|
+
const tabActive = ctxRef.current?.tabActive?.value;
|
|
82
|
+
|
|
83
|
+
return pageActive !== false && tabActive !== false;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 组件卸载时统一清理所有挂起任务,避免异步回调在卸载后继续运行。
|
|
37
87
|
useEffect(() => {
|
|
38
88
|
return () => {
|
|
89
|
+
if (pendingTimerRef.current) {
|
|
90
|
+
clearTimeout(pendingTimerRef.current);
|
|
91
|
+
pendingTimerRef.current = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
39
94
|
if (pendingDisposerRef.current) {
|
|
40
95
|
pendingDisposerRef.current();
|
|
41
96
|
pendingDisposerRef.current = null;
|
|
@@ -47,38 +102,45 @@ export const observer = <P, Options extends IObserverOptions = IObserverOptions>
|
|
|
47
102
|
() =>
|
|
48
103
|
originalObserver(Component, {
|
|
49
104
|
scheduler(updater) {
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
if (!isContextActive()) {
|
|
106
|
+
if (pendingTimerRef.current || pendingDisposerRef.current) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 通过异步任务打断同步调度,避免连续触发时形成递归更新。
|
|
111
|
+
pendingTimerRef.current = setTimeout(() => {
|
|
112
|
+
pendingTimerRef.current = null;
|
|
52
113
|
|
|
53
|
-
if (pageActive === false || tabActive === false) {
|
|
54
|
-
// Avoid stack overflow
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
// If there is already a pending updater, do nothing
|
|
57
114
|
if (pendingDisposerRef.current) {
|
|
58
115
|
return;
|
|
59
116
|
}
|
|
60
117
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
118
|
+
if (isContextActive()) {
|
|
119
|
+
updater();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 只监听组合后的“是否可更新”状态,条件恢复后执行一次并立即销毁。
|
|
124
|
+
pendingDisposerRef.current = reaction(
|
|
125
|
+
() => isContextActive(),
|
|
126
|
+
(active) => {
|
|
127
|
+
if (!active) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
clearPendingDisposer();
|
|
67
132
|
updater();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'FlowObserverPendingUpdate',
|
|
136
|
+
},
|
|
137
|
+
);
|
|
73
138
|
});
|
|
74
139
|
return;
|
|
75
140
|
}
|
|
76
141
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
pendingDisposerRef.current();
|
|
80
|
-
pendingDisposerRef.current = null;
|
|
81
|
-
}
|
|
142
|
+
clearPendingTimer();
|
|
143
|
+
clearPendingDisposer();
|
|
82
144
|
|
|
83
145
|
updater();
|
|
84
146
|
},
|
|
@@ -11,18 +11,18 @@ import type { SnippetModule } from '../../types';
|
|
|
11
11
|
const snippet: SnippetModule = {
|
|
12
12
|
contexts: ['*'],
|
|
13
13
|
scenes: ['detailFieldEvent', 'formFieldEvent'],
|
|
14
|
-
prefix: 'sn-
|
|
15
|
-
label: 'Set
|
|
16
|
-
description: 'Customize form and
|
|
14
|
+
prefix: 'sn-item-style',
|
|
15
|
+
label: 'Set form item/details item style',
|
|
16
|
+
description: 'Customize form item and details item container styles',
|
|
17
17
|
locales: {
|
|
18
18
|
'zh-CN': {
|
|
19
|
-
label: '
|
|
20
|
-
description: '
|
|
19
|
+
label: '设置表单项/详情项样式',
|
|
20
|
+
description: '自定义表单项和详情项容器样式',
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
content: `
|
|
24
|
-
ctx.model.
|
|
25
|
-
|
|
24
|
+
ctx.model.props.style = {
|
|
25
|
+
background: 'red',
|
|
26
26
|
};
|
|
27
27
|
`,
|
|
28
28
|
};
|
package/src/types.ts
CHANGED
|
@@ -407,22 +407,34 @@ export type FlowModelLoaderResult =
|
|
|
407
407
|
export type FlowModelLoader = () => Promise<FlowModelLoaderResult>;
|
|
408
408
|
|
|
409
409
|
/**
|
|
410
|
-
* FlowModel loader entry.
|
|
411
|
-
* Future contribution-style fields are intentionally kept as commented placeholders in phase A
|
|
412
|
-
* and are not consumed by current runtime logic.
|
|
410
|
+
* FlowModel loader entry (normalized internal form).
|
|
413
411
|
*/
|
|
414
412
|
export interface FlowModelLoaderEntry {
|
|
415
413
|
loader: FlowModelLoader;
|
|
416
|
-
|
|
414
|
+
extends?: string[];
|
|
417
415
|
// meta?: Partial<FlowModelMeta>;
|
|
418
416
|
// scenes?: string[];
|
|
419
417
|
}
|
|
420
418
|
|
|
421
419
|
/**
|
|
422
|
-
* FlowModel loader
|
|
420
|
+
* FlowModel loader input (user-facing form for registerModelLoaders).
|
|
421
|
+
* The `extends` field accepts flexible formats that will be normalized to `string[]` at registration time.
|
|
422
|
+
*/
|
|
423
|
+
export interface FlowModelLoaderInput {
|
|
424
|
+
loader: FlowModelLoader;
|
|
425
|
+
extends?: string | ModelConstructor | (string | ModelConstructor)[];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* FlowModel loader entry map (normalized internal form).
|
|
423
430
|
*/
|
|
424
431
|
export type FlowModelLoaderMap = Record<string, FlowModelLoaderEntry>;
|
|
425
432
|
|
|
433
|
+
/**
|
|
434
|
+
* FlowModel loader input map (user-facing form for registerModelLoaders).
|
|
435
|
+
*/
|
|
436
|
+
export type FlowModelLoaderInputMap = Record<string, FlowModelLoaderInput>;
|
|
437
|
+
|
|
426
438
|
/**
|
|
427
439
|
* Batch ensure result.
|
|
428
440
|
*/
|
|
@@ -437,17 +449,6 @@ export interface EnsureBatchResult {
|
|
|
437
449
|
|
|
438
450
|
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
|
|
439
451
|
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
|
|
440
|
-
/**
|
|
441
|
-
* Ensure a model exists (create if missing) in a single request.
|
|
442
|
-
*/
|
|
443
|
-
ensure: (
|
|
444
|
-
values: Record<string, any>,
|
|
445
|
-
options?: { includeAsyncNode?: boolean },
|
|
446
|
-
) => Promise<Record<string, any> | null>;
|
|
447
|
-
/**
|
|
448
|
-
* Optional: run multiple ops in a single transaction (server capability).
|
|
449
|
-
*/
|
|
450
|
-
mutate?: (values: Record<string, any>, options?: { includeAsyncNode?: boolean }) => Promise<Record<string, any>>;
|
|
451
452
|
save(model: T, options?: { onlyStepParams?: boolean }): Promise<Record<string, any>>;
|
|
452
453
|
destroy(uid: string): Promise<boolean>;
|
|
453
454
|
move(sourceId: string, targetId: string, position: 'before' | 'after'): Promise<void>;
|