@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.11
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/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +6 -0
- package/lib/FlowSchemaRegistry.d.ts +154 -0
- package/lib/FlowSchemaRegistry.js +1427 -0
- package/lib/JSRunner.d.ts +15 -0
- package/lib/JSRunner.js +82 -7
- package/lib/ViewScopedFlowEngine.js +8 -1
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +59 -3
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +21 -3
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +84 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +190 -26
- package/lib/flow-schema-registry/fieldBinding.d.ts +32 -0
- package/lib/flow-schema-registry/fieldBinding.js +165 -0
- package/lib/flow-schema-registry/modelPatches.d.ts +16 -0
- package/lib/flow-schema-registry/modelPatches.js +235 -0
- package/lib/flow-schema-registry/schemaInference.d.ts +17 -0
- package/lib/flow-schema-registry/schemaInference.js +207 -0
- package/lib/flow-schema-registry/utils.d.ts +25 -0
- package/lib/flow-schema-registry/utils.js +293 -0
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2270 -148
- package/lib/flowEngine.d.ts +160 -1
- package/lib/flowEngine.js +387 -27
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +51 -17
- package/lib/index.d.ts +8 -1
- package/lib/index.js +24 -1
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- 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 +7 -0
- package/lib/models/flowModel.js +83 -8
- package/lib/provider.js +7 -6
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +23 -9
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +7 -1
- package/lib/scheduler/ModelOperationScheduler.js +28 -23
- package/lib/server.d.ts +10 -0
- package/lib/server.js +32 -0
- package/lib/types.d.ts +296 -1
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +49 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +17 -1
- package/lib/utils/schema-utils.js +80 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- 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 +28 -6
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +27 -5
- package/lib/views/usePage.d.ts +6 -1
- package/lib/views/usePage.js +53 -9
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +5 -5
- package/server.d.ts +1 -0
- package/server.js +1 -0
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/FlowSchemaRegistry.ts +1799 -0
- package/src/JSRunner.ts +111 -5
- package/src/ViewScopedFlowEngine.ts +8 -0
- package/src/__tests__/FlowSchemaRegistry.test.ts +1951 -0
- package/src/__tests__/JSRunner.test.ts +91 -1
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flow-engine.test.ts +48 -0
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowEngine.modelLoaders.test.ts +249 -0
- package/src/__tests__/flowEngine.saveModel.test.ts +4 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +26 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +5 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +72 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
- package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
- package/src/components/dnd/gridDragPlanner.ts +68 -2
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +31 -4
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +143 -32
- package/src/components/subModel/utils.ts +1 -1
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +88 -110
- package/src/executor/FlowExecutor.ts +230 -28
- package/src/executor/__tests__/flowExecutor.test.ts +123 -0
- package/src/flow-schema-registry/fieldBinding.ts +171 -0
- package/src/flow-schema-registry/modelPatches.ts +260 -0
- package/src/flow-schema-registry/schemaInference.ts +210 -0
- package/src/flow-schema-registry/utils.ts +268 -0
- package/src/flowContext.ts +2989 -212
- package/src/flowEngine.ts +434 -23
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +58 -18
- package/src/index.ts +15 -1
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- 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__/dispatchEvent.when.test.ts +768 -0
- package/src/models/__tests__/flowModel.clone.test.ts +416 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +112 -7
- package/src/provider.tsx +9 -7
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +25 -9
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +41 -24
- package/src/server.ts +11 -0
- package/src/types.ts +359 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +157 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +38 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +109 -1
- package/src/views/FlowView.tsx +11 -1
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +34 -5
- package/src/views/useDrawer.tsx +33 -4
- package/src/views/usePage.tsx +63 -8
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -0,0 +1,63 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../flowEngine';
|
|
12
|
+
import { MultiRecordResource } from '../resources/multiRecordResource';
|
|
13
|
+
import { SingleRecordResource } from '../resources/singleRecordResource';
|
|
14
|
+
|
|
15
|
+
describe('FlowEngine dataSource dirty registry', () => {
|
|
16
|
+
it('tracks versions per dataSourceKey + resourceName', () => {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
|
|
19
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(0);
|
|
20
|
+
expect(engine.markDataSourceDirty('main', 'posts')).toBe(1);
|
|
21
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
22
|
+
|
|
23
|
+
expect(engine.markDataSourceDirty('main', 'posts')).toBe(2);
|
|
24
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
|
|
25
|
+
|
|
26
|
+
// different resource
|
|
27
|
+
expect(engine.getDataSourceDirtyVersion('main', 'users')).toBe(0);
|
|
28
|
+
expect(engine.markDataSourceDirty('main', 'users')).toBe(1);
|
|
29
|
+
expect(engine.getDataSourceDirtyVersion('main', 'users')).toBe(1);
|
|
30
|
+
|
|
31
|
+
// different data source
|
|
32
|
+
expect(engine.getDataSourceDirtyVersion('ds2', 'posts')).toBe(0);
|
|
33
|
+
expect(engine.markDataSourceDirty('ds2', 'posts')).toBe(1);
|
|
34
|
+
expect(engine.getDataSourceDirtyVersion('ds2', 'posts')).toBe(1);
|
|
35
|
+
// main unchanged
|
|
36
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('marks dirty on record write operations (single & multi)', async () => {
|
|
40
|
+
const engine = new FlowEngine();
|
|
41
|
+
const markSpy = vi.spyOn(engine, 'markDataSourceDirty');
|
|
42
|
+
|
|
43
|
+
const single = engine.createResource(SingleRecordResource);
|
|
44
|
+
single.setDataSourceKey('main');
|
|
45
|
+
single.setResourceName('posts');
|
|
46
|
+
// avoid network: stub runAction + refresh
|
|
47
|
+
(single as any).runAction = vi.fn().mockResolvedValue({ data: {}, meta: {} });
|
|
48
|
+
(single as any).refresh = vi.fn().mockResolvedValue(undefined);
|
|
49
|
+
await single.save({ title: 't' } as any, { refresh: false });
|
|
50
|
+
expect(markSpy).toHaveBeenCalledWith('main', 'posts');
|
|
51
|
+
|
|
52
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
53
|
+
multi.setDataSourceKey('main');
|
|
54
|
+
multi.setResourceName('users.profile');
|
|
55
|
+
(multi as any).runAction = vi.fn().mockResolvedValue({ data: [], meta: {} });
|
|
56
|
+
(multi as any).refresh = vi.fn().mockResolvedValue(undefined);
|
|
57
|
+
await multi.create({ name: 'n' } as any, { refresh: false });
|
|
58
|
+
// exact association
|
|
59
|
+
expect(markSpy).toHaveBeenCalledWith('main', 'users.profile');
|
|
60
|
+
// plus root collection (safety)
|
|
61
|
+
expect(markSpy).toHaveBeenCalledWith('main', 'users');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
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(_query?: any) {
|
|
20
|
+
return this.findOneResult ? JSON.parse(JSON.stringify(this.findOneResult)) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async ensure(options: any) {
|
|
24
|
+
return await this.findOne(options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async destroy() {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async move() {}
|
|
32
|
+
|
|
33
|
+
async duplicate() {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('FlowEngine model loaders', () => {
|
|
39
|
+
let engine: FlowEngine;
|
|
40
|
+
let repo: MockFlowModelRepository;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
engine = new FlowEngine();
|
|
44
|
+
repo = new MockFlowModelRepository();
|
|
45
|
+
engine.setModelRepository(repo);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('resolves explicit and meta-default model trees before synchronous creation', async () => {
|
|
49
|
+
class ParentModel extends FlowModel {}
|
|
50
|
+
class ChildModel extends FlowModel {}
|
|
51
|
+
class DefaultChildModel extends FlowModel {}
|
|
52
|
+
|
|
53
|
+
ParentModel.define({
|
|
54
|
+
createModelOptions: {
|
|
55
|
+
subModels: {
|
|
56
|
+
defaultChild: {
|
|
57
|
+
use: 'DefaultChildModel',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const parentLoader = vi.fn(async () => ({ ParentModel }));
|
|
64
|
+
const childLoader = vi.fn(async () => ({ ChildModel }));
|
|
65
|
+
const defaultChildLoader = vi.fn(async () => ({ DefaultChildModel }));
|
|
66
|
+
|
|
67
|
+
engine.registerModelLoaders({
|
|
68
|
+
ParentModel: { loader: parentLoader },
|
|
69
|
+
ChildModel: { loader: childLoader },
|
|
70
|
+
DefaultChildModel: { loader: defaultChildLoader },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const model = await engine.loadOrCreateModel({
|
|
74
|
+
uid: 'parent-model',
|
|
75
|
+
use: 'ParentModel',
|
|
76
|
+
subModels: {
|
|
77
|
+
child: {
|
|
78
|
+
use: 'ChildModel',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(model).toBeInstanceOf(ParentModel);
|
|
84
|
+
expect(model?.subModels.child).toBeInstanceOf(ChildModel);
|
|
85
|
+
expect(model?.subModels.defaultChild).toBeInstanceOf(DefaultChildModel);
|
|
86
|
+
expect(parentLoader).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(defaultChildLoader).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('resolves repository-loaded model trees before loadModel creates instances', async () => {
|
|
92
|
+
class RepoRootModel extends FlowModel {}
|
|
93
|
+
class RepoChildModel extends FlowModel {}
|
|
94
|
+
|
|
95
|
+
const rootLoader = vi.fn(async () => ({ RepoRootModel }));
|
|
96
|
+
const childLoader = vi.fn(async () => ({ RepoChildModel }));
|
|
97
|
+
|
|
98
|
+
engine.registerModelLoaders({
|
|
99
|
+
RepoRootModel: { loader: rootLoader },
|
|
100
|
+
RepoChildModel: { loader: childLoader },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
repo.findOneResult = {
|
|
104
|
+
uid: 'repo-root',
|
|
105
|
+
use: 'RepoRootModel',
|
|
106
|
+
subModels: {
|
|
107
|
+
child: {
|
|
108
|
+
use: 'RepoChildModel',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const model = await engine.loadModel({ uid: 'repo-root' });
|
|
114
|
+
|
|
115
|
+
expect(model).toBeInstanceOf(RepoRootModel);
|
|
116
|
+
expect(model?.subModels.child).toBeInstanceOf(RepoChildModel);
|
|
117
|
+
expect(rootLoader).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('supports async model creation and async getters', async () => {
|
|
122
|
+
class AsyncRootModel extends FlowModel {}
|
|
123
|
+
class AsyncChildModel extends FlowModel {}
|
|
124
|
+
|
|
125
|
+
const rootLoader = vi.fn(async () => ({ AsyncRootModel }));
|
|
126
|
+
const childLoader = vi.fn(async () => ({ AsyncChildModel }));
|
|
127
|
+
|
|
128
|
+
engine.registerModelLoaders({
|
|
129
|
+
AsyncRootModel: { loader: rootLoader },
|
|
130
|
+
AsyncChildModel: { loader: childLoader },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const rootClass = await engine.getModelClassAsync('AsyncRootModel');
|
|
134
|
+
const classes = await engine.getModelClassesAsync();
|
|
135
|
+
const model = await engine.createModelAsync({
|
|
136
|
+
uid: 'async-root',
|
|
137
|
+
use: 'AsyncRootModel',
|
|
138
|
+
subModels: {
|
|
139
|
+
child: {
|
|
140
|
+
use: 'AsyncChildModel',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(rootClass).toBe(AsyncRootModel);
|
|
146
|
+
expect(classes.get('AsyncRootModel')).toBe(AsyncRootModel);
|
|
147
|
+
expect(classes.get('AsyncChildModel')).toBe(AsyncChildModel);
|
|
148
|
+
expect(model).toBeInstanceOf(AsyncRootModel);
|
|
149
|
+
expect(model.subModels.child).toBeInstanceOf(AsyncChildModel);
|
|
150
|
+
expect(rootLoader).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('createModelAsync degrades unresolved loader failures to ErrorFlowModel', async () => {
|
|
155
|
+
const invalidLoader = vi.fn(async () => ({ notAModel: {} }));
|
|
156
|
+
|
|
157
|
+
engine.registerModelLoaders({
|
|
158
|
+
BrokenRootModel: { loader: invalidLoader as any },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const model = await engine.createModelAsync({
|
|
162
|
+
uid: 'broken-root',
|
|
163
|
+
use: 'BrokenRootModel',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(model).toBeInstanceOf(ErrorFlowModel);
|
|
167
|
+
expect(invalidLoader).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('keeps loader resolution idempotent across resolveModelTree and flow settings preload', async () => {
|
|
171
|
+
class RuntimeResolvedModel extends FlowModel {}
|
|
172
|
+
class DesignResolvedModel extends FlowModel {}
|
|
173
|
+
|
|
174
|
+
const runtimeLoader = vi.fn(async () => ({ RuntimeResolvedModel }));
|
|
175
|
+
const designLoader = vi.fn(async () => ({ DesignResolvedModel }));
|
|
176
|
+
|
|
177
|
+
engine.registerModelLoaders({
|
|
178
|
+
RuntimeResolvedModel: { loader: runtimeLoader },
|
|
179
|
+
DesignResolvedModel: { loader: designLoader },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await engine.resolveModelTree({
|
|
183
|
+
use: 'RuntimeResolvedModel',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const firstPreload = await engine.preloadModelLoaders();
|
|
187
|
+
const secondPreload = await engine.preloadModelLoaders();
|
|
188
|
+
|
|
189
|
+
expect(runtimeLoader).toHaveBeenCalledTimes(1);
|
|
190
|
+
expect(designLoader).toHaveBeenCalledTimes(1);
|
|
191
|
+
expect(firstPreload.loaded).toContain('DesignResolvedModel');
|
|
192
|
+
expect(firstPreload.loaded).not.toContain('RuntimeResolvedModel');
|
|
193
|
+
expect(secondPreload.loaded).toHaveLength(0);
|
|
194
|
+
expect(secondPreload.failed).toHaveLength(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('picks up newly registered loaders after preload has already completed', async () => {
|
|
198
|
+
class FirstModel extends FlowModel {}
|
|
199
|
+
class SecondModel extends FlowModel {}
|
|
200
|
+
|
|
201
|
+
const firstLoader = vi.fn(async () => ({ FirstModel }));
|
|
202
|
+
const secondLoader = vi.fn(async () => ({ SecondModel }));
|
|
203
|
+
|
|
204
|
+
engine.registerModelLoaders({
|
|
205
|
+
FirstModel: { loader: firstLoader },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await engine.preloadModelLoaders();
|
|
209
|
+
expect(firstLoader).toHaveBeenCalledTimes(1);
|
|
210
|
+
expect(engine.getModelClass('FirstModel')).toBe(FirstModel);
|
|
211
|
+
|
|
212
|
+
engine.registerModelLoaders({
|
|
213
|
+
SecondModel: { loader: secondLoader },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const result = await engine.preloadModelLoaders();
|
|
217
|
+
|
|
218
|
+
expect(secondLoader).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(result.loaded).toContain('SecondModel');
|
|
220
|
+
expect(engine.getModelClass('SecondModel')).toBe(SecondModel);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('degrades unresolved loader failures to ErrorFlowModel instead of crashing runtime creation', async () => {
|
|
224
|
+
class ParentModel extends FlowModel {}
|
|
225
|
+
|
|
226
|
+
const parentLoader = vi.fn(async () => ({ ParentModel }));
|
|
227
|
+
const invalidChildLoader = vi.fn(async () => ({ notAModel: {} }));
|
|
228
|
+
|
|
229
|
+
engine.registerModelLoaders({
|
|
230
|
+
ParentModel: { loader: parentLoader },
|
|
231
|
+
BrokenChildModel: { loader: invalidChildLoader as any },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const model = await engine.loadOrCreateModel({
|
|
235
|
+
uid: 'parent-with-broken-child',
|
|
236
|
+
use: 'ParentModel',
|
|
237
|
+
subModels: {
|
|
238
|
+
child: {
|
|
239
|
+
use: 'BrokenChildModel',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(model).toBeInstanceOf(ParentModel);
|
|
245
|
+
expect(model?.subModels.child).toBeInstanceOf(ErrorFlowModel);
|
|
246
|
+
expect(parentLoader).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(invalidChildLoader).toHaveBeenCalledTimes(1);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -76,4 +76,32 @@ describe('FlowModelContext.openView - navigation enforcement', () => {
|
|
|
76
76
|
const dispatchedParams = child.dispatchEvent.mock.calls[0][1];
|
|
77
77
|
expect(dispatchedParams.navigation).toBe(false);
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
it('dispatches the popupSettings bound event (object form) when opening external popup', async () => {
|
|
81
|
+
const { parent, child } = setup();
|
|
82
|
+
|
|
83
|
+
child.getFlow = vi.fn((key: string) => {
|
|
84
|
+
if (key !== 'popupSettings') return undefined;
|
|
85
|
+
return { on: { eventName: 'openDuplicatePopup' } };
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await (parent.context as any).openView('child-uid', { mode: 'drawer' });
|
|
89
|
+
|
|
90
|
+
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
91
|
+
expect(child.dispatchEvent.mock.calls[0][0]).toBe('openDuplicatePopup');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('falls back to click when popupSettings has no explicit on event', async () => {
|
|
95
|
+
const { parent, child } = setup();
|
|
96
|
+
|
|
97
|
+
child.getFlow = vi.fn((key: string) => {
|
|
98
|
+
if (key !== 'popupSettings') return undefined;
|
|
99
|
+
return { on: undefined };
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await (parent.context as any).openView('child-uid', { mode: 'drawer' });
|
|
103
|
+
|
|
104
|
+
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(child.dispatchEvent.mock.calls[0][0]).toBe('click');
|
|
106
|
+
});
|
|
79
107
|
});
|
|
@@ -61,6 +61,69 @@ describe('FlowRunJSContext.define() and getDoc() deep tests', () => {
|
|
|
61
61
|
expect(apiProp.properties?.auth?.description).toBe('Auth info');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it('should treat string doc as description when overriding object docs', () => {
|
|
65
|
+
class BaseDocContext extends FlowRunJSContext {}
|
|
66
|
+
class ChildDocContext extends BaseDocContext {}
|
|
67
|
+
|
|
68
|
+
BaseDocContext.define({
|
|
69
|
+
properties: {
|
|
70
|
+
foo: {
|
|
71
|
+
description: 'Base foo',
|
|
72
|
+
detail: 'FooDetail',
|
|
73
|
+
hidden: true,
|
|
74
|
+
completion: { insertText: 'ctx.foo' },
|
|
75
|
+
properties: {
|
|
76
|
+
bar: { description: 'Bar desc' },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
ChildDocContext.define({
|
|
83
|
+
properties: {
|
|
84
|
+
foo: 'Child foo',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const doc = ChildDocContext.getDoc();
|
|
89
|
+
const foo: any = doc.properties?.foo;
|
|
90
|
+
expect(foo).toBeTruthy();
|
|
91
|
+
expect(typeof foo).toBe('object');
|
|
92
|
+
expect(foo.description).toBe('Child foo');
|
|
93
|
+
expect(foo.hidden).toBe(true);
|
|
94
|
+
expect(foo.detail).toBe('FooDetail');
|
|
95
|
+
expect(foo.completion?.insertText).toBe('ctx.foo');
|
|
96
|
+
expect(foo.properties?.bar?.description).toBe('Bar desc');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should preserve base string description when patch adds object doc', () => {
|
|
100
|
+
class BaseStringDocContext extends FlowRunJSContext {}
|
|
101
|
+
class ChildObjectDocContext extends BaseStringDocContext {}
|
|
102
|
+
|
|
103
|
+
BaseStringDocContext.define({
|
|
104
|
+
properties: {
|
|
105
|
+
foo: 'Base foo',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
ChildObjectDocContext.define({
|
|
110
|
+
properties: {
|
|
111
|
+
foo: {
|
|
112
|
+
properties: {
|
|
113
|
+
bar: 'Bar',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const doc = ChildObjectDocContext.getDoc();
|
|
120
|
+
const foo: any = doc.properties?.foo;
|
|
121
|
+
expect(foo).toBeTruthy();
|
|
122
|
+
expect(typeof foo).toBe('object');
|
|
123
|
+
expect(foo.description).toBe('Base foo');
|
|
124
|
+
expect(foo.properties?.bar).toBe('Bar');
|
|
125
|
+
});
|
|
126
|
+
|
|
64
127
|
it('should deep merge method documentation', () => {
|
|
65
128
|
class MethodMergeContext extends FlowRunJSContext {}
|
|
66
129
|
|
|
@@ -11,6 +11,7 @@ import { ContextPathProxy } from '../ContextPathProxy';
|
|
|
11
11
|
import { FlowRuntimeContext } from '../flowContext';
|
|
12
12
|
import { FlowEngine } from '../flowEngine';
|
|
13
13
|
import { FlowModel } from '../models/flowModel';
|
|
14
|
+
import { FlowExitAllException } from '../utils/exceptions';
|
|
14
15
|
|
|
15
16
|
describe('FlowRuntimeContext', () => {
|
|
16
17
|
let engine: FlowEngine;
|
|
@@ -46,7 +47,7 @@ describe('FlowRuntimeContext', () => {
|
|
|
46
47
|
|
|
47
48
|
it('should throw on exit()', () => {
|
|
48
49
|
const ctx = new FlowRuntimeContext(model, 'flow1', 'runtime');
|
|
49
|
-
expect(() => ctx.exit()).toThrow();
|
|
50
|
+
expect(() => ctx.exit()).toThrow(FlowExitAllException);
|
|
50
51
|
});
|
|
51
52
|
});
|
|
52
53
|
|
|
@@ -12,6 +12,7 @@ import { screen } from '@testing-library/react';
|
|
|
12
12
|
import { FlowSettings } from '../flowSettings';
|
|
13
13
|
import { FlowModel } from '../models';
|
|
14
14
|
import { FlowEngine } from '../flowEngine';
|
|
15
|
+
import { GLOBAL_EMBED_CONTAINER_ID } from '../views';
|
|
15
16
|
|
|
16
17
|
// We will stub viewer directly on model.context in tests
|
|
17
18
|
|
|
@@ -1087,18 +1088,18 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1087
1088
|
|
|
1088
1089
|
// Create mock DOM element for embed target
|
|
1089
1090
|
const mockTarget = document.createElement('div');
|
|
1090
|
-
mockTarget.id =
|
|
1091
|
+
mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
1091
1092
|
mockTarget.style.width = 'auto';
|
|
1092
1093
|
mockTarget.style.maxWidth = 'none';
|
|
1093
1094
|
document.body.appendChild(mockTarget);
|
|
1094
1095
|
|
|
1095
1096
|
// Mock querySelector to return our mock element
|
|
1096
|
-
const originalQuerySelector = document.querySelector;
|
|
1097
|
-
|
|
1098
|
-
if (selector ===
|
|
1097
|
+
const originalQuerySelector = document.querySelector.bind(document);
|
|
1098
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
1099
|
+
if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
|
|
1099
1100
|
return mockTarget;
|
|
1100
1101
|
}
|
|
1101
|
-
return originalQuerySelector
|
|
1102
|
+
return originalQuerySelector(selector);
|
|
1102
1103
|
});
|
|
1103
1104
|
|
|
1104
1105
|
const M = model.constructor as any;
|
|
@@ -1164,7 +1165,61 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1164
1165
|
|
|
1165
1166
|
// Cleanup
|
|
1166
1167
|
document.body.removeChild(mockTarget);
|
|
1167
|
-
|
|
1168
|
+
querySelectorSpy.mockRestore();
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it('does not clear embed target DOM before opening (avoids portal unmount errors)', async () => {
|
|
1172
|
+
const engine = new FlowEngine();
|
|
1173
|
+
const flowSettings = new FlowSettings(engine);
|
|
1174
|
+
const model = new FlowModel({ uid: 'm-embed-no-clear', flowEngine: engine });
|
|
1175
|
+
|
|
1176
|
+
const mockTarget = document.createElement('div');
|
|
1177
|
+
mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
1178
|
+
mockTarget.innerHTML = '<div data-testid="existing">Existing</div>';
|
|
1179
|
+
document.body.appendChild(mockTarget);
|
|
1180
|
+
|
|
1181
|
+
const originalQuerySelector = document.querySelector.bind(document);
|
|
1182
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
1183
|
+
if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
|
|
1184
|
+
return mockTarget;
|
|
1185
|
+
}
|
|
1186
|
+
return originalQuerySelector(selector);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
const M = model.constructor as any;
|
|
1190
|
+
M.registerFlow({
|
|
1191
|
+
key: 'embedNoClearFlow',
|
|
1192
|
+
steps: {
|
|
1193
|
+
step: {
|
|
1194
|
+
title: 'Step',
|
|
1195
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const embed = vi.fn((opts: any) => {
|
|
1201
|
+
// The existing DOM should not be wiped out before opening the embed view.
|
|
1202
|
+
expect(mockTarget.querySelector('[data-testid="existing"]')).toBeTruthy();
|
|
1203
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1204
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1205
|
+
return dlg;
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
model.context.defineProperty('viewer', { value: { embed } });
|
|
1209
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1210
|
+
|
|
1211
|
+
await flowSettings.open({
|
|
1212
|
+
model,
|
|
1213
|
+
flowKey: 'embedNoClearFlow',
|
|
1214
|
+
stepKey: 'step',
|
|
1215
|
+
uiMode: 'embed',
|
|
1216
|
+
} as any);
|
|
1217
|
+
|
|
1218
|
+
expect(embed).toHaveBeenCalledTimes(1);
|
|
1219
|
+
expect(mockTarget.querySelector('[data-testid="existing"]')).toBeTruthy();
|
|
1220
|
+
|
|
1221
|
+
document.body.removeChild(mockTarget);
|
|
1222
|
+
querySelectorSpy.mockRestore();
|
|
1168
1223
|
});
|
|
1169
1224
|
|
|
1170
1225
|
it('uses embed uiMode with default props when target element exists', async () => {
|
|
@@ -1174,16 +1229,16 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1174
1229
|
|
|
1175
1230
|
// Create mock DOM element for embed target
|
|
1176
1231
|
const mockTarget = document.createElement('div');
|
|
1177
|
-
mockTarget.id =
|
|
1232
|
+
mockTarget.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
1178
1233
|
document.body.appendChild(mockTarget);
|
|
1179
1234
|
|
|
1180
1235
|
// Mock querySelector
|
|
1181
|
-
const originalQuerySelector = document.querySelector;
|
|
1182
|
-
|
|
1183
|
-
if (selector ===
|
|
1236
|
+
const originalQuerySelector = document.querySelector.bind(document);
|
|
1237
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
1238
|
+
if (selector === `#${GLOBAL_EMBED_CONTAINER_ID}`) {
|
|
1184
1239
|
return mockTarget;
|
|
1185
1240
|
}
|
|
1186
|
-
return originalQuerySelector
|
|
1241
|
+
return originalQuerySelector(selector);
|
|
1187
1242
|
});
|
|
1188
1243
|
|
|
1189
1244
|
const M = model.constructor as any;
|
|
@@ -1223,7 +1278,7 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1223
1278
|
|
|
1224
1279
|
// Cleanup
|
|
1225
1280
|
document.body.removeChild(mockTarget);
|
|
1226
|
-
|
|
1281
|
+
querySelectorSpy.mockRestore();
|
|
1227
1282
|
});
|
|
1228
1283
|
|
|
1229
1284
|
it('handles embed uiMode when target element is not found', async () => {
|
|
@@ -1232,8 +1287,7 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1232
1287
|
const model = new FlowModel({ uid: 'm-embed-no-target', flowEngine: engine });
|
|
1233
1288
|
|
|
1234
1289
|
// Mock querySelector to return null (target not found)
|
|
1235
|
-
const
|
|
1236
|
-
document.querySelector = vi.fn(() => null);
|
|
1290
|
+
const querySelectorSpy = vi.spyOn(document, 'querySelector').mockReturnValue(null);
|
|
1237
1291
|
|
|
1238
1292
|
const M = model.constructor as any;
|
|
1239
1293
|
M.registerFlow({
|
|
@@ -1266,7 +1320,7 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1266
1320
|
expect(embed).toHaveBeenCalledTimes(1);
|
|
1267
1321
|
|
|
1268
1322
|
// Restore querySelector
|
|
1269
|
-
|
|
1323
|
+
querySelectorSpy.mockRestore();
|
|
1270
1324
|
});
|
|
1271
1325
|
|
|
1272
1326
|
it('handles error in function-based step uiMode gracefully', async () => {
|
|
@@ -1718,8 +1772,8 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1718
1772
|
expect(capturedDialog.close).toHaveBeenCalled();
|
|
1719
1773
|
});
|
|
1720
1774
|
|
|
1721
|
-
it('submit method handles
|
|
1722
|
-
const {
|
|
1775
|
+
it('submit method handles FlowExitAllException by closing dialog without error message', async () => {
|
|
1776
|
+
const { FlowExitAllException } = await import('../utils/exceptions');
|
|
1723
1777
|
|
|
1724
1778
|
const engine = new FlowEngine();
|
|
1725
1779
|
const flowSettings = new FlowSettings(engine);
|
|
@@ -1732,7 +1786,7 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1732
1786
|
step: {
|
|
1733
1787
|
title: 'Step',
|
|
1734
1788
|
beforeParamsSave: () => {
|
|
1735
|
-
throw new
|
|
1789
|
+
throw new FlowExitAllException('exitFlow', 'm-submit-exit', 'Exit requested');
|
|
1736
1790
|
},
|
|
1737
1791
|
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
1738
1792
|
},
|
|
@@ -1764,12 +1818,62 @@ describe('FlowSettings.open rendering behavior', () => {
|
|
|
1764
1818
|
// Call submit method
|
|
1765
1819
|
await capturedDialog.submit();
|
|
1766
1820
|
|
|
1767
|
-
// Verify
|
|
1821
|
+
// Verify FlowExitAllException handling
|
|
1768
1822
|
expect(error).not.toHaveBeenCalled(); // Should not show error message
|
|
1769
1823
|
expect(success).not.toHaveBeenCalled(); // Should not show success message
|
|
1770
1824
|
expect(capturedDialog.close).toHaveBeenCalled(); // Should close dialog
|
|
1771
1825
|
});
|
|
1772
1826
|
|
|
1827
|
+
it('submit method keeps dialog open on FlowCancelSaveException', async () => {
|
|
1828
|
+
const { FlowCancelSaveException } = await import('../utils/exceptions');
|
|
1829
|
+
|
|
1830
|
+
const engine = new FlowEngine();
|
|
1831
|
+
const flowSettings = new FlowSettings(engine);
|
|
1832
|
+
const TestFlowModel = createIsolatedFlowModel('test-submit-cancel-save');
|
|
1833
|
+
const model = new TestFlowModel({ uid: 'm-submit-cancel-save', flowEngine: engine });
|
|
1834
|
+
|
|
1835
|
+
TestFlowModel.registerFlow({
|
|
1836
|
+
key: 'cancelSaveFlow',
|
|
1837
|
+
steps: {
|
|
1838
|
+
step: {
|
|
1839
|
+
title: 'Step',
|
|
1840
|
+
beforeParamsSave: () => {
|
|
1841
|
+
throw new FlowCancelSaveException();
|
|
1842
|
+
},
|
|
1843
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
1844
|
+
},
|
|
1845
|
+
},
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
const info = vi.fn();
|
|
1849
|
+
const error = vi.fn();
|
|
1850
|
+
const success = vi.fn();
|
|
1851
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
1852
|
+
|
|
1853
|
+
const saveStepParams = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
|
|
1854
|
+
|
|
1855
|
+
let capturedDialog: any;
|
|
1856
|
+
model.context.defineProperty('viewer', {
|
|
1857
|
+
value: {
|
|
1858
|
+
dialog: ({ content }) => {
|
|
1859
|
+
capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
|
|
1860
|
+
if (typeof content === 'function') {
|
|
1861
|
+
content(capturedDialog, { defineMethod: vi.fn() });
|
|
1862
|
+
}
|
|
1863
|
+
return capturedDialog;
|
|
1864
|
+
},
|
|
1865
|
+
},
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
await flowSettings.open({ model, flowKey: 'cancelSaveFlow', stepKey: 'step' } as any);
|
|
1869
|
+
await capturedDialog.submit();
|
|
1870
|
+
|
|
1871
|
+
expect(capturedDialog.close).not.toHaveBeenCalled();
|
|
1872
|
+
expect(saveStepParams).not.toHaveBeenCalled();
|
|
1873
|
+
expect(error).not.toHaveBeenCalled();
|
|
1874
|
+
expect(success).not.toHaveBeenCalled();
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1773
1877
|
it('submit method handles general errors by showing error message and keeping dialog open', async () => {
|
|
1774
1878
|
const engine = new FlowEngine();
|
|
1775
1879
|
const flowSettings = new FlowSettings(engine);
|