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