@nocobase/flow-engine 2.1.0-beta.9 → 2.2.0-beta.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
|
@@ -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';
|
|
@@ -159,6 +160,23 @@ describe('FlowContext properties and methods', () => {
|
|
|
159
160
|
expect(ctx.shared).toBe('from delegate');
|
|
160
161
|
});
|
|
161
162
|
|
|
163
|
+
it('should expose current language as a top-level variable', async () => {
|
|
164
|
+
const engine = new FlowEngine();
|
|
165
|
+
const ctx = engine.context;
|
|
166
|
+
ctx.defineProperty('api', { value: { auth: { locale: 'zh-CN' } } });
|
|
167
|
+
ctx.defineProperty('i18n', { value: { language: 'en-US' } });
|
|
168
|
+
|
|
169
|
+
expect(ctx.locale).toBe('zh-CN');
|
|
170
|
+
await expect(ctx.resolveJsonTemplate('{{ ctx.locale }}')).resolves.toBe('zh-CN');
|
|
171
|
+
|
|
172
|
+
const localeNode = ctx.getPropertyMetaTree().find((node) => node.name === 'locale');
|
|
173
|
+
expect(localeNode).toMatchObject({
|
|
174
|
+
name: 'locale',
|
|
175
|
+
title: '{{t("Current language")}}',
|
|
176
|
+
paths: ['locale'],
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
162
180
|
it('should throw sync error in get', () => {
|
|
163
181
|
const ctx = new FlowContext();
|
|
164
182
|
ctx.defineProperty('error', {
|
|
@@ -778,6 +796,29 @@ describe('FlowContext.getApiInfos', () => {
|
|
|
778
796
|
expect((infos.bar?.ref as any)?.url).toBe('https://example.com');
|
|
779
797
|
});
|
|
780
798
|
|
|
799
|
+
it('should include completion only when requested by getApiInfos()', async () => {
|
|
800
|
+
const ctx = new FlowContext();
|
|
801
|
+
ctx.defineMethod('bar', () => 2, {
|
|
802
|
+
description: 'Bar',
|
|
803
|
+
completion: { insertText: 'ctx.bar()' },
|
|
804
|
+
});
|
|
805
|
+
ctx.defineProperty('token', {
|
|
806
|
+
value: 't',
|
|
807
|
+
info: {
|
|
808
|
+
description: 'Token string',
|
|
809
|
+
completion: { insertText: 'ctx.token' },
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const compact = await ctx.getApiInfos();
|
|
814
|
+
expect((compact.bar as any)?.completion).toBeUndefined();
|
|
815
|
+
expect((compact.token as any)?.completion).toBeUndefined();
|
|
816
|
+
|
|
817
|
+
const editorInfos = await ctx.getApiInfos({ includeCompletion: true });
|
|
818
|
+
expect((editorInfos.bar as any)?.completion?.insertText).toBe('ctx.bar()');
|
|
819
|
+
expect((editorInfos.token as any)?.completion?.insertText).toBe('ctx.token');
|
|
820
|
+
});
|
|
821
|
+
|
|
781
822
|
it('should return property infos with completion/ref/examples', async () => {
|
|
782
823
|
const ctx = new FlowContext();
|
|
783
824
|
ctx.defineProperty('token', {
|
|
@@ -1630,6 +1671,69 @@ describe('runAction delegation from runtime context', () => {
|
|
|
1630
1671
|
});
|
|
1631
1672
|
});
|
|
1632
1673
|
|
|
1674
|
+
describe('FlowContext request defaults', () => {
|
|
1675
|
+
class RequestModel extends FlowModel {}
|
|
1676
|
+
|
|
1677
|
+
afterEach(() => {
|
|
1678
|
+
vi.restoreAllMocks();
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
const createRequestContext = () => {
|
|
1682
|
+
const engine = new FlowEngine();
|
|
1683
|
+
engine.registerModels({ RequestModel });
|
|
1684
|
+
|
|
1685
|
+
const apiRequest = vi.fn(async (options) => options);
|
|
1686
|
+
const app = {
|
|
1687
|
+
getApiUrl(pathname = '') {
|
|
1688
|
+
return 'https://app.example.com/api/'.replace(/\/$/g, '') + '/' + pathname.replace(/^\//g, '');
|
|
1689
|
+
},
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
engine.context.defineProperty('api', { value: { request: apiRequest } as any });
|
|
1693
|
+
engine.context.defineProperty('app', { value: app });
|
|
1694
|
+
|
|
1695
|
+
const model = engine.createModel({ use: 'RequestModel' });
|
|
1696
|
+
const ctx = new FlowRuntimeContext(model, 'flow');
|
|
1697
|
+
const directAxiosRequest = vi.spyOn(axios, 'request').mockResolvedValue({ data: {} } as any);
|
|
1698
|
+
|
|
1699
|
+
return { ctx, apiRequest, directAxiosRequest };
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
it.each([
|
|
1703
|
+
['apiClient', 'users:list', 'api'],
|
|
1704
|
+
['apiClient', '/api/users:list', 'api'],
|
|
1705
|
+
['apiClient', 'https://app.example.com/api/users:list', 'api'],
|
|
1706
|
+
['direct axios', 'https://app.example.com/custom-api/users', 'axios'],
|
|
1707
|
+
])('should use %s for %s', async (_target, url, expected) => {
|
|
1708
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1709
|
+
|
|
1710
|
+
await ctx.request({ url, method: 'get' });
|
|
1711
|
+
|
|
1712
|
+
if (expected === 'api') {
|
|
1713
|
+
expect(apiRequest).toHaveBeenCalledTimes(1);
|
|
1714
|
+
expect(directAxiosRequest).not.toHaveBeenCalled();
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1719
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
it('should use direct axios for cross-origin absolute urls', async () => {
|
|
1723
|
+
const { ctx, apiRequest, directAxiosRequest } = createRequestContext();
|
|
1724
|
+
|
|
1725
|
+
await ctx.request({ url: 'https://api.example.com/users', method: 'get', skipAuth: false });
|
|
1726
|
+
|
|
1727
|
+
expect(directAxiosRequest).toHaveBeenCalledTimes(1);
|
|
1728
|
+
expect(apiRequest).not.toHaveBeenCalled();
|
|
1729
|
+
expect(directAxiosRequest.mock.calls[0][0]).toMatchObject({
|
|
1730
|
+
url: 'https://api.example.com/users',
|
|
1731
|
+
method: 'get',
|
|
1732
|
+
skipAuth: false,
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1633
1737
|
describe('FlowContext delayed meta loading', () => {
|
|
1634
1738
|
// 测试场景:属性定义时 meta 为异步函数,首次访问时延迟加载
|
|
1635
1739
|
// 输入:属性带有异步 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,9 +8,30 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { reaction } from '@nocobase/flow-engine';
|
|
11
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { FlowEngine } from '../flowEngine';
|
|
13
13
|
import { FlowModel } from '../models';
|
|
14
|
+
import type { IFlowModelRepository } from '../types';
|
|
15
|
+
|
|
16
|
+
class MoveRepository implements IFlowModelRepository {
|
|
17
|
+
move = vi.fn(async (_sourceId: string, _targetId: string, _position: 'before' | 'after'): Promise<void> => {});
|
|
18
|
+
|
|
19
|
+
async findOne(): Promise<Record<string, unknown> | null> {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async save(): Promise<Record<string, unknown>> {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async destroy(): Promise<boolean> {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async duplicate(): Promise<Record<string, unknown> | null> {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
14
35
|
|
|
15
36
|
describe('FlowEngine moveModel', () => {
|
|
16
37
|
let engine: FlowEngine;
|
|
@@ -20,6 +41,14 @@ describe('FlowEngine moveModel', () => {
|
|
|
20
41
|
engine.registerModels({ FlowModel });
|
|
21
42
|
});
|
|
22
43
|
|
|
44
|
+
const createParentWithChildren = () => {
|
|
45
|
+
const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
|
|
46
|
+
parent.addSubModel('items', { uid: 'child-a', use: 'FlowModel' });
|
|
47
|
+
parent.addSubModel('items', { uid: 'child-b', use: 'FlowModel' });
|
|
48
|
+
parent.addSubModel('items', { uid: 'child-c', use: 'FlowModel' });
|
|
49
|
+
return parent;
|
|
50
|
+
};
|
|
51
|
+
|
|
23
52
|
it('keeps subModels array reactive after move so later additions trigger reactions', async () => {
|
|
24
53
|
const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
|
|
25
54
|
parent.addSubModel('items', { uid: 'child-a', use: 'FlowModel' });
|
|
@@ -40,4 +69,55 @@ describe('FlowEngine moveModel', () => {
|
|
|
40
69
|
dispose();
|
|
41
70
|
expect(seen).toEqual([3]);
|
|
42
71
|
});
|
|
72
|
+
|
|
73
|
+
it('persists an after move when dragging forward', async () => {
|
|
74
|
+
const repository = new MoveRepository();
|
|
75
|
+
engine.setModelRepository(repository);
|
|
76
|
+
const parent = createParentWithChildren();
|
|
77
|
+
|
|
78
|
+
await engine.moveModel('child-a', 'child-c');
|
|
79
|
+
|
|
80
|
+
expect(repository.move).toHaveBeenCalledWith('child-a', 'child-c', 'after');
|
|
81
|
+
expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-b', 'child-c', 'child-a']);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('persists a before move when dragging backward', async () => {
|
|
85
|
+
const repository = new MoveRepository();
|
|
86
|
+
engine.setModelRepository(repository);
|
|
87
|
+
const parent = createParentWithChildren();
|
|
88
|
+
|
|
89
|
+
await engine.moveModel('child-c', 'child-a');
|
|
90
|
+
|
|
91
|
+
expect(repository.move).toHaveBeenCalledWith('child-c', 'child-a', 'before');
|
|
92
|
+
expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-c', 'child-a', 'child-b']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not persist self-drop', async () => {
|
|
96
|
+
const repository = new MoveRepository();
|
|
97
|
+
engine.setModelRepository(repository);
|
|
98
|
+
const parent = createParentWithChildren();
|
|
99
|
+
|
|
100
|
+
await engine.moveModel('child-a', 'child-a');
|
|
101
|
+
|
|
102
|
+
expect(repository.move).not.toHaveBeenCalled();
|
|
103
|
+
expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-a', 'child-b', 'child-c']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('keeps null sortIndex subModels in stable order', () => {
|
|
107
|
+
const parent = engine.createModel({
|
|
108
|
+
uid: 'parent',
|
|
109
|
+
use: 'FlowModel',
|
|
110
|
+
subModels: {
|
|
111
|
+
items: [
|
|
112
|
+
{ uid: 'child-a', use: 'FlowModel', sortIndex: null as unknown as number },
|
|
113
|
+
{ uid: 'child-b', use: 'FlowModel', sortIndex: null as unknown as number },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
parent.addSubModel('items', { uid: 'child-c', use: 'FlowModel' });
|
|
119
|
+
|
|
120
|
+
expect((parent.subModels.items as FlowModel[]).map((item) => item.uid)).toEqual(['child-a', 'child-b', 'child-c']);
|
|
121
|
+
expect((parent.subModels.items as FlowModel[]).map((item) => item.sortIndex)).toEqual([1, 2, 3]);
|
|
122
|
+
});
|
|
43
123
|
});
|