@nocobase/flow-engine 2.1.0-alpha.2 → 2.1.0-alpha.20
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/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- package/lib/components/FlowModelRenderer.d.ts +1 -1
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.js +6 -2
- 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/StepSettingsDialog.js +16 -2
- 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/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/data-source/index.js +6 -0
- package/lib/executor/FlowExecutor.js +31 -8
- package/lib/flowContext.js +31 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- 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/flowModel.js +17 -7
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +20 -12
- 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/scheduler/ModelOperationScheduler.d.ts +5 -1
- package/lib/scheduler/ModelOperationScheduler.js +3 -2
- package/lib/types.d.ts +47 -1
- package/lib/utils/index.d.ts +2 -2
- package/lib/utils/index.js +4 -0
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- 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/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 +20 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +20 -3
- package/lib/views/usePage.d.ts +2 -1
- package/lib/views/usePage.js +10 -3
- package/package.json +6 -5
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +65 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +16 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/FlowModelRenderer.tsx +3 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +88 -0
- package/src/components/dnd/gridDragPlanner.ts +8 -2
- 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/StepSettingsDialog.tsx +18 -2
- 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/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
- package/src/components/subModel/utils.ts +1 -1
- package/src/data-source/index.ts +6 -0
- package/src/executor/FlowExecutor.ts +34 -9
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flowContext.ts +35 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowSettings.ts +40 -6
- 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/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/flowModel.tsx +18 -6
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +22 -12
- 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/scheduler/ModelOperationScheduler.ts +14 -3
- package/src/types.ts +60 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- 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 +11 -1
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +25 -3
- package/src/views/useDrawer.tsx +25 -3
- package/src/views/usePage.tsx +12 -3
|
@@ -189,4 +189,170 @@ describe('FlowEngine', () => {
|
|
|
189
189
|
expect(mounted?.uid).toBe('c3');
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
describe('getSubclassesOfAsync', () => {
|
|
194
|
+
it('should return async-loaded subclasses matching extends declaration', async () => {
|
|
195
|
+
class AsyncSubModelD extends BaseModel {}
|
|
196
|
+
class AsyncSubModelE extends BaseModel {}
|
|
197
|
+
|
|
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
|
+
|
|
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
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
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);
|
|
241
|
+
});
|
|
242
|
+
|
|
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(() => {});
|
|
281
|
+
|
|
282
|
+
engine.registerModelLoaders({
|
|
283
|
+
UnrelatedModel: {
|
|
284
|
+
extends: 'BaseModel',
|
|
285
|
+
loader: async () => ({ UnrelatedModel }),
|
|
286
|
+
},
|
|
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"),
|
|
293
|
+
);
|
|
294
|
+
|
|
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);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
192
358
|
});
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
11
12
|
import { FlowContext, FlowRuntimeContext, FlowRunJSContext, type PropertyMetaFactory } from '../flowContext';
|
|
12
13
|
import { FlowEngine } from '../flowEngine';
|
|
13
14
|
import { FlowModel } from '../models/flowModel';
|
|
@@ -1630,6 +1631,69 @@ describe('runAction delegation from runtime context', () => {
|
|
|
1630
1631
|
});
|
|
1631
1632
|
});
|
|
1632
1633
|
|
|
1634
|
+
describe('FlowContext request defaults', () => {
|
|
1635
|
+
class RequestModel extends FlowModel {}
|
|
1636
|
+
|
|
1637
|
+
afterEach(() => {
|
|
1638
|
+
vi.restoreAllMocks();
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
const createRequestContext = () => {
|
|
1642
|
+
const engine = new FlowEngine();
|
|
1643
|
+
engine.registerModels({ RequestModel });
|
|
1644
|
+
|
|
1645
|
+
const apiRequest = vi.fn(async (options) => options);
|
|
1646
|
+
const app = {
|
|
1647
|
+
getApiUrl(pathname = '') {
|
|
1648
|
+
return 'https://app.example.com/api/'.replace(/\/$/g, '') + '/' + pathname.replace(/^\//g, '');
|
|
1649
|
+
},
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
engine.context.defineProperty('api', { value: { request: apiRequest } as any });
|
|
1653
|
+
engine.context.defineProperty('app', { value: app });
|
|
1654
|
+
|
|
1655
|
+
const model = engine.createModel({ use: 'RequestModel' });
|
|
1656
|
+
const ctx = new FlowRuntimeContext(model, 'flow');
|
|
1657
|
+
const directAxiosRequest = vi.spyOn(axios, 'request').mockResolvedValue({ data: {} } as any);
|
|
1658
|
+
|
|
1659
|
+
return { ctx, apiRequest, directAxiosRequest };
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
it.each([
|
|
1663
|
+
['apiClient', 'users:list', 'api'],
|
|
1664
|
+
['apiClient', '/api/users:list', 'api'],
|
|
1665
|
+
['apiClient', 'https://app.example.com/api/users:list', 'api'],
|
|
1666
|
+
['direct axios', 'https://app.example.com/custom-api/users', 'axios'],
|
|
1667
|
+
])('should use %s for %s', async (_target, url, expected) => {
|
|
1668
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1669
|
+
|
|
1670
|
+
await ctx.request({ url, method: 'get' });
|
|
1671
|
+
|
|
1672
|
+
if (expected === 'api') {
|
|
1673
|
+
expect(apiRequest).toHaveBeenCalledTimes(1);
|
|
1674
|
+
expect(directAxiosRequest).not.toHaveBeenCalled();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1679
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it('should use direct axios for cross-origin absolute urls', async () => {
|
|
1683
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1684
|
+
|
|
1685
|
+
await ctx.request({ url: 'https://api.example.com/users', method: 'get', skipAuth: false });
|
|
1686
|
+
|
|
1687
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1688
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1689
|
+
expect(directAxiosRequest.mock.calls[0][0]).toMatchObject({
|
|
1690
|
+
url: 'https://api.example.com/users',
|
|
1691
|
+
method: 'get',
|
|
1692
|
+
skipAuth: false,
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1633
1697
|
describe('FlowContext delayed meta loading', () => {
|
|
1634
1698
|
// 测试场景:属性定义时 meta 为异步函数,首次访问时延迟加载
|
|
1635
1699
|
// 输入:属性带有异步 meta 函数
|
|
@@ -0,0 +1,245 @@
|
|
|
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 { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../flowEngine';
|
|
12
|
+
import { ErrorFlowModel, FlowModel } from '../models';
|
|
13
|
+
import type { IFlowModelRepository } from '../types';
|
|
14
|
+
|
|
15
|
+
class MockFlowModelRepository implements IFlowModelRepository {
|
|
16
|
+
findOneResult: any = null;
|
|
17
|
+
save = vi.fn(async (model: FlowModel) => ({ success: true, uid: model.uid }));
|
|
18
|
+
|
|
19
|
+
async findOne() {
|
|
20
|
+
return this.findOneResult ? JSON.parse(JSON.stringify(this.findOneResult)) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async destroy() {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async move() {}
|
|
28
|
+
|
|
29
|
+
async duplicate() {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('FlowEngine model loaders', () => {
|
|
35
|
+
let engine: FlowEngine;
|
|
36
|
+
let repo: MockFlowModelRepository;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
engine = new FlowEngine();
|
|
40
|
+
repo = new MockFlowModelRepository();
|
|
41
|
+
engine.setModelRepository(repo);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('resolves explicit and meta-default model trees before synchronous creation', async () => {
|
|
45
|
+
class ParentModel extends FlowModel {}
|
|
46
|
+
class ChildModel extends FlowModel {}
|
|
47
|
+
class DefaultChildModel extends FlowModel {}
|
|
48
|
+
|
|
49
|
+
ParentModel.define({
|
|
50
|
+
createModelOptions: {
|
|
51
|
+
subModels: {
|
|
52
|
+
defaultChild: {
|
|
53
|
+
use: 'DefaultChildModel',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const parentLoader = vi.fn(async () => ({ ParentModel }));
|
|
60
|
+
const childLoader = vi.fn(async () => ({ ChildModel }));
|
|
61
|
+
const defaultChildLoader = vi.fn(async () => ({ DefaultChildModel }));
|
|
62
|
+
|
|
63
|
+
engine.registerModelLoaders({
|
|
64
|
+
ParentModel: { loader: parentLoader },
|
|
65
|
+
ChildModel: { loader: childLoader },
|
|
66
|
+
DefaultChildModel: { loader: defaultChildLoader },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const model = await engine.loadOrCreateModel({
|
|
70
|
+
uid: 'parent-model',
|
|
71
|
+
use: 'ParentModel',
|
|
72
|
+
subModels: {
|
|
73
|
+
child: {
|
|
74
|
+
use: 'ChildModel',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(model).toBeInstanceOf(ParentModel);
|
|
80
|
+
expect(model?.subModels.child).toBeInstanceOf(ChildModel);
|
|
81
|
+
expect(model?.subModels.defaultChild).toBeInstanceOf(DefaultChildModel);
|
|
82
|
+
expect(parentLoader).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(defaultChildLoader).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('resolves repository-loaded model trees before loadModel creates instances', async () => {
|
|
88
|
+
class RepoRootModel extends FlowModel {}
|
|
89
|
+
class RepoChildModel extends FlowModel {}
|
|
90
|
+
|
|
91
|
+
const rootLoader = vi.fn(async () => ({ RepoRootModel }));
|
|
92
|
+
const childLoader = vi.fn(async () => ({ RepoChildModel }));
|
|
93
|
+
|
|
94
|
+
engine.registerModelLoaders({
|
|
95
|
+
RepoRootModel: { loader: rootLoader },
|
|
96
|
+
RepoChildModel: { loader: childLoader },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
repo.findOneResult = {
|
|
100
|
+
uid: 'repo-root',
|
|
101
|
+
use: 'RepoRootModel',
|
|
102
|
+
subModels: {
|
|
103
|
+
child: {
|
|
104
|
+
use: 'RepoChildModel',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const model = await engine.loadModel({ uid: 'repo-root' });
|
|
110
|
+
|
|
111
|
+
expect(model).toBeInstanceOf(RepoRootModel);
|
|
112
|
+
expect(model?.subModels.child).toBeInstanceOf(RepoChildModel);
|
|
113
|
+
expect(rootLoader).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('supports async model creation and async getters', async () => {
|
|
118
|
+
class AsyncRootModel extends FlowModel {}
|
|
119
|
+
class AsyncChildModel extends FlowModel {}
|
|
120
|
+
|
|
121
|
+
const rootLoader = vi.fn(async () => ({ AsyncRootModel }));
|
|
122
|
+
const childLoader = vi.fn(async () => ({ AsyncChildModel }));
|
|
123
|
+
|
|
124
|
+
engine.registerModelLoaders({
|
|
125
|
+
AsyncRootModel: { loader: rootLoader },
|
|
126
|
+
AsyncChildModel: { loader: childLoader },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const rootClass = await engine.getModelClassAsync('AsyncRootModel');
|
|
130
|
+
const classes = await engine.getModelClassesAsync();
|
|
131
|
+
const model = await engine.createModelAsync({
|
|
132
|
+
uid: 'async-root',
|
|
133
|
+
use: 'AsyncRootModel',
|
|
134
|
+
subModels: {
|
|
135
|
+
child: {
|
|
136
|
+
use: 'AsyncChildModel',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(rootClass).toBe(AsyncRootModel);
|
|
142
|
+
expect(classes.get('AsyncRootModel')).toBe(AsyncRootModel);
|
|
143
|
+
expect(classes.get('AsyncChildModel')).toBe(AsyncChildModel);
|
|
144
|
+
expect(model).toBeInstanceOf(AsyncRootModel);
|
|
145
|
+
expect(model.subModels.child).toBeInstanceOf(AsyncChildModel);
|
|
146
|
+
expect(rootLoader).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('createModelAsync degrades unresolved loader failures to ErrorFlowModel', async () => {
|
|
151
|
+
const invalidLoader = vi.fn(async () => ({ notAModel: {} }));
|
|
152
|
+
|
|
153
|
+
engine.registerModelLoaders({
|
|
154
|
+
BrokenRootModel: { loader: invalidLoader as any },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const model = await engine.createModelAsync({
|
|
158
|
+
uid: 'broken-root',
|
|
159
|
+
use: 'BrokenRootModel',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(model).toBeInstanceOf(ErrorFlowModel);
|
|
163
|
+
expect(invalidLoader).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('keeps loader resolution idempotent across resolveModelTree and flow settings preload', async () => {
|
|
167
|
+
class RuntimeResolvedModel extends FlowModel {}
|
|
168
|
+
class DesignResolvedModel extends FlowModel {}
|
|
169
|
+
|
|
170
|
+
const runtimeLoader = vi.fn(async () => ({ RuntimeResolvedModel }));
|
|
171
|
+
const designLoader = vi.fn(async () => ({ DesignResolvedModel }));
|
|
172
|
+
|
|
173
|
+
engine.registerModelLoaders({
|
|
174
|
+
RuntimeResolvedModel: { loader: runtimeLoader },
|
|
175
|
+
DesignResolvedModel: { loader: designLoader },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await engine.resolveModelTree({
|
|
179
|
+
use: 'RuntimeResolvedModel',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const firstPreload = await engine.preloadModelLoaders();
|
|
183
|
+
const secondPreload = await engine.preloadModelLoaders();
|
|
184
|
+
|
|
185
|
+
expect(runtimeLoader).toHaveBeenCalledTimes(1);
|
|
186
|
+
expect(designLoader).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(firstPreload.loaded).toContain('DesignResolvedModel');
|
|
188
|
+
expect(firstPreload.loaded).not.toContain('RuntimeResolvedModel');
|
|
189
|
+
expect(secondPreload.loaded).toHaveLength(0);
|
|
190
|
+
expect(secondPreload.failed).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('picks up newly registered loaders after preload has already completed', async () => {
|
|
194
|
+
class FirstModel extends FlowModel {}
|
|
195
|
+
class SecondModel extends FlowModel {}
|
|
196
|
+
|
|
197
|
+
const firstLoader = vi.fn(async () => ({ FirstModel }));
|
|
198
|
+
const secondLoader = vi.fn(async () => ({ SecondModel }));
|
|
199
|
+
|
|
200
|
+
engine.registerModelLoaders({
|
|
201
|
+
FirstModel: { loader: firstLoader },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await engine.preloadModelLoaders();
|
|
205
|
+
expect(firstLoader).toHaveBeenCalledTimes(1);
|
|
206
|
+
expect(engine.getModelClass('FirstModel')).toBe(FirstModel);
|
|
207
|
+
|
|
208
|
+
engine.registerModelLoaders({
|
|
209
|
+
SecondModel: { loader: secondLoader },
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const result = await engine.preloadModelLoaders();
|
|
213
|
+
|
|
214
|
+
expect(secondLoader).toHaveBeenCalledTimes(1);
|
|
215
|
+
expect(result.loaded).toContain('SecondModel');
|
|
216
|
+
expect(engine.getModelClass('SecondModel')).toBe(SecondModel);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('degrades unresolved loader failures to ErrorFlowModel instead of crashing runtime creation', async () => {
|
|
220
|
+
class ParentModel extends FlowModel {}
|
|
221
|
+
|
|
222
|
+
const parentLoader = vi.fn(async () => ({ ParentModel }));
|
|
223
|
+
const invalidChildLoader = vi.fn(async () => ({ notAModel: {} }));
|
|
224
|
+
|
|
225
|
+
engine.registerModelLoaders({
|
|
226
|
+
ParentModel: { loader: parentLoader },
|
|
227
|
+
BrokenChildModel: { loader: invalidChildLoader as any },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const model = await engine.loadOrCreateModel({
|
|
231
|
+
uid: 'parent-with-broken-child',
|
|
232
|
+
use: 'ParentModel',
|
|
233
|
+
subModels: {
|
|
234
|
+
child: {
|
|
235
|
+
use: 'BrokenChildModel',
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(model).toBeInstanceOf(ParentModel);
|
|
241
|
+
expect(model?.subModels.child).toBeInstanceOf(ErrorFlowModel);
|
|
242
|
+
expect(parentLoader).toHaveBeenCalledTimes(1);
|
|
243
|
+
expect(invalidChildLoader).toHaveBeenCalledTimes(1);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { createForm } from '@formily/core';
|
|
13
|
+
import { createSchemaField, FormProvider } from '@formily/react';
|
|
14
|
+
import { render, screen } from '@testing-library/react';
|
|
11
15
|
import { FlowSettings } from '../flowSettings';
|
|
12
16
|
import { DefaultSettingsIcon } from '../components/settings/wrappers/contextual/DefaultSettingsIcon';
|
|
13
17
|
import { FlowModel } from '../models';
|
|
@@ -142,10 +146,10 @@ describe('FlowSettings', () => {
|
|
|
142
146
|
expect(settingsItem?.sort).toBe(0);
|
|
143
147
|
});
|
|
144
148
|
|
|
145
|
-
test('should set up observable properties', () => {
|
|
149
|
+
test('should set up observable properties', async () => {
|
|
146
150
|
// Test that enabled property is reactive
|
|
147
151
|
const initialEnabled = flowSettings.enabled;
|
|
148
|
-
flowSettings.enable();
|
|
152
|
+
await flowSettings.enable();
|
|
149
153
|
expect(flowSettings.enabled).not.toBe(initialEnabled);
|
|
150
154
|
expect(flowSettings.enabled).toBe(true);
|
|
151
155
|
});
|
|
@@ -186,6 +190,43 @@ describe('FlowSettings', () => {
|
|
|
186
190
|
flowSettings.registerComponents({});
|
|
187
191
|
expect(Object.keys(flowSettings.components)).toHaveLength(0);
|
|
188
192
|
});
|
|
193
|
+
|
|
194
|
+
test('should register component loaders and load component on render', async () => {
|
|
195
|
+
const loader = vi.fn(async () => ({
|
|
196
|
+
default: () => React.createElement('div', null, 'Lazy Flow Settings Component'),
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
flowSettings.registerComponentLoaders({
|
|
200
|
+
DemoFlowSettingsLazyField: loader,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(loader).not.toHaveBeenCalled();
|
|
204
|
+
|
|
205
|
+
const SchemaField = createSchemaField();
|
|
206
|
+
const form = createForm();
|
|
207
|
+
|
|
208
|
+
render(
|
|
209
|
+
React.createElement(
|
|
210
|
+
FormProvider,
|
|
211
|
+
{ form },
|
|
212
|
+
React.createElement(SchemaField, {
|
|
213
|
+
schema: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
demo: {
|
|
217
|
+
type: 'void',
|
|
218
|
+
'x-component': 'DemoFlowSettingsLazyField',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
components: flowSettings.components,
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(await screen.findByText('Lazy Flow Settings Component')).toBeInTheDocument();
|
|
228
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
229
|
+
});
|
|
189
230
|
});
|
|
190
231
|
|
|
191
232
|
describe('Scope Registration', () => {
|
|
@@ -228,30 +269,68 @@ describe('FlowSettings', () => {
|
|
|
228
269
|
});
|
|
229
270
|
|
|
230
271
|
describe('Enable/Disable Functionality', () => {
|
|
231
|
-
test('should enable flow settings', () => {
|
|
272
|
+
test('should enable flow settings', async () => {
|
|
232
273
|
expect(flowSettings.enabled).toBe(false);
|
|
233
274
|
|
|
234
|
-
flowSettings.enable();
|
|
275
|
+
await flowSettings.enable();
|
|
235
276
|
|
|
236
277
|
expect(flowSettings.enabled).toBe(true);
|
|
237
278
|
});
|
|
238
279
|
|
|
239
|
-
test('should
|
|
240
|
-
|
|
280
|
+
test('should preload model loaders before enabling flow settings', async () => {
|
|
281
|
+
const preloadSpy = vi.spyOn(engine, 'preloadModelLoaders').mockResolvedValue({
|
|
282
|
+
requested: [],
|
|
283
|
+
loaded: [],
|
|
284
|
+
failed: [],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await flowSettings.enable();
|
|
288
|
+
|
|
289
|
+
expect(preloadSpy).toHaveBeenCalledTimes(1);
|
|
241
290
|
expect(flowSettings.enabled).toBe(true);
|
|
291
|
+
});
|
|
242
292
|
|
|
243
|
-
|
|
293
|
+
test('should preload model loaders before force enabling flow settings', async () => {
|
|
294
|
+
const preloadSpy = vi.spyOn(engine, 'preloadModelLoaders').mockResolvedValue({
|
|
295
|
+
requested: [],
|
|
296
|
+
loaded: [],
|
|
297
|
+
failed: [],
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await flowSettings.forceEnable();
|
|
301
|
+
|
|
302
|
+
expect(preloadSpy).toHaveBeenCalledTimes(1);
|
|
303
|
+
expect(flowSettings.enabled).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('should disable flow settings', async () => {
|
|
307
|
+
await flowSettings.enable();
|
|
308
|
+
expect(flowSettings.enabled).toBe(true);
|
|
309
|
+
|
|
310
|
+
await flowSettings.disable();
|
|
244
311
|
|
|
245
312
|
expect(flowSettings.enabled).toBe(false);
|
|
246
313
|
});
|
|
247
314
|
|
|
248
|
-
test('should handle multiple enable/disable calls', () => {
|
|
249
|
-
flowSettings.enable();
|
|
250
|
-
flowSettings.enable();
|
|
315
|
+
test('should handle multiple enable/disable calls', async () => {
|
|
316
|
+
await flowSettings.enable();
|
|
317
|
+
await flowSettings.enable();
|
|
251
318
|
expect(flowSettings.enabled).toBe(true);
|
|
252
319
|
|
|
253
|
-
flowSettings.disable();
|
|
254
|
-
flowSettings.disable();
|
|
320
|
+
await flowSettings.disable();
|
|
321
|
+
await flowSettings.disable();
|
|
322
|
+
expect(flowSettings.enabled).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('forceDisable should clear force-enabled state and disable flow settings', async () => {
|
|
326
|
+
await flowSettings.forceEnable();
|
|
327
|
+
expect(flowSettings.enabled).toBe(true);
|
|
328
|
+
|
|
329
|
+
await flowSettings.forceDisable();
|
|
330
|
+
|
|
331
|
+
expect(flowSettings.enabled).toBe(false);
|
|
332
|
+
|
|
333
|
+
await flowSettings.disable();
|
|
255
334
|
expect(flowSettings.enabled).toBe(false);
|
|
256
335
|
});
|
|
257
336
|
});
|
|
@@ -512,7 +591,7 @@ describe('FlowSettings', () => {
|
|
|
512
591
|
});
|
|
513
592
|
|
|
514
593
|
describe('Complex Integration Scenarios', () => {
|
|
515
|
-
test('should maintain state consistency during multiple operations', () => {
|
|
594
|
+
test('should maintain state consistency during multiple operations', async () => {
|
|
516
595
|
// Initialize with components and scopes
|
|
517
596
|
const TestComponent = () => 'TestComponent';
|
|
518
597
|
const testScope = () => 'testScope';
|
|
@@ -528,7 +607,7 @@ describe('FlowSettings', () => {
|
|
|
528
607
|
});
|
|
529
608
|
|
|
530
609
|
// Enable/disable
|
|
531
|
-
flowSettings.enable();
|
|
610
|
+
await flowSettings.enable();
|
|
532
611
|
expect(flowSettings.enabled).toBe(true);
|
|
533
612
|
|
|
534
613
|
// Verify all state is maintained
|
|
@@ -536,7 +615,7 @@ describe('FlowSettings', () => {
|
|
|
536
615
|
expect(flowSettings.scopes.testScope).toBe(testScope);
|
|
537
616
|
expect(flowSettings.getToolbarItems().find((item) => item.key === 'integration-test')).toBeDefined();
|
|
538
617
|
|
|
539
|
-
flowSettings.disable();
|
|
618
|
+
await flowSettings.disable();
|
|
540
619
|
expect(flowSettings.enabled).toBe(false);
|
|
541
620
|
|
|
542
621
|
// State should still be maintained after disable
|