@nocobase/flow-engine 2.0.0-alpha.2
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 +661 -0
- package/README.md +30 -0
- package/lib/ContextPathProxy.d.ts +17 -0
- package/lib/ContextPathProxy.js +65 -0
- package/lib/ElementProxy.d.ts +17 -0
- package/lib/ElementProxy.js +93 -0
- package/lib/FlowContextProvider.d.ts +24 -0
- package/lib/FlowContextProvider.js +82 -0
- package/lib/FlowDefinition.d.ts +423 -0
- package/lib/FlowDefinition.js +257 -0
- package/lib/JSRunner.d.ts +32 -0
- package/lib/JSRunner.js +95 -0
- package/lib/ReactView.d.ts +20 -0
- package/lib/ReactView.js +120 -0
- package/lib/ViewScopedFlowEngine.d.ts +23 -0
- package/lib/ViewScopedFlowEngine.js +81 -0
- package/lib/acl/Acl.d.ts +31 -0
- package/lib/acl/Acl.js +115 -0
- package/lib/action-registry/BaseActionRegistry.d.ts +23 -0
- package/lib/action-registry/BaseActionRegistry.js +57 -0
- package/lib/action-registry/EngineActionRegistry.d.ts +20 -0
- package/lib/action-registry/EngineActionRegistry.js +47 -0
- package/lib/action-registry/ModelActionRegistry.d.ts +34 -0
- package/lib/action-registry/ModelActionRegistry.js +79 -0
- package/lib/components/DynamicFlowsEditor.d.ts +17 -0
- package/lib/components/DynamicFlowsEditor.js +49 -0
- package/lib/components/FieldModelRenderer.d.ts +10 -0
- package/lib/components/FieldModelRenderer.js +94 -0
- package/lib/components/FlowContextSelector.d.ts +11 -0
- package/lib/components/FlowContextSelector.js +221 -0
- package/lib/components/FlowErrorFallback.d.ts +25 -0
- package/lib/components/FlowErrorFallback.js +264 -0
- package/lib/components/FlowModelRenderer.d.ts +64 -0
- package/lib/components/FlowModelRenderer.js +254 -0
- package/lib/components/FormItem.d.ts +18 -0
- package/lib/components/FormItem.js +147 -0
- package/lib/components/common/FlowSettingsButton.d.ts +11 -0
- package/lib/components/common/FlowSettingsButton.js +66 -0
- package/lib/components/common/index.d.ts +9 -0
- package/lib/components/common/index.js +30 -0
- package/lib/components/common/withFlowDesignMode.d.ts +26 -0
- package/lib/components/common/withFlowDesignMode.js +61 -0
- package/lib/components/dnd/getMousePositionOnElement.d.ts +50 -0
- package/lib/components/dnd/getMousePositionOnElement.js +95 -0
- package/lib/components/dnd/index.d.ts +24 -0
- package/lib/components/dnd/index.js +164 -0
- package/lib/components/dnd/moveBlock.d.ts +33 -0
- package/lib/components/dnd/moveBlock.js +302 -0
- package/lib/components/index.d.ts +18 -0
- package/lib/components/index.js +48 -0
- package/lib/components/settings/independents/dropdown/FlowsDropdownButton.d.ts +46 -0
- package/lib/components/settings/independents/dropdown/FlowsDropdownButton.js +225 -0
- package/lib/components/settings/independents/dropdown/index.d.ts +9 -0
- package/lib/components/settings/independents/dropdown/index.js +30 -0
- package/lib/components/settings/independents/index.d.ts +1 -0
- package/lib/components/settings/independents/index.js +30 -0
- package/lib/components/settings/index.d.ts +10 -0
- package/lib/components/settings/index.js +32 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +24 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +501 -0
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.d.ts +45 -0
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +231 -0
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +111 -0
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +484 -0
- package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.d.ts +26 -0
- package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.js +342 -0
- package/lib/components/settings/wrappers/contextual/StepSettings.d.ts +23 -0
- package/lib/components/settings/wrappers/contextual/StepSettings.js +110 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.d.ts +20 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +206 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.d.ts +20 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.js +47 -0
- package/lib/components/settings/wrappers/contextual/index.d.ts +14 -0
- package/lib/components/settings/wrappers/contextual/index.js +40 -0
- package/lib/components/settings/wrappers/embedded/FlowSettings.d.ts +33 -0
- package/lib/components/settings/wrappers/embedded/FlowSettings.js +207 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettings.d.ts +41 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettings.js +84 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.d.ts +16 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.js +88 -0
- package/lib/components/settings/wrappers/embedded/index.d.ts +10 -0
- package/lib/components/settings/wrappers/embedded/index.js +32 -0
- package/lib/components/settings/wrappers/index.d.ts +2 -0
- package/lib/components/settings/wrappers/index.js +32 -0
- package/lib/components/subModel/AddSubModelButton.d.ts +62 -0
- package/lib/components/subModel/AddSubModelButton.js +415 -0
- package/lib/components/subModel/LazyDropdown.d.ts +55 -0
- package/lib/components/subModel/LazyDropdown.js +524 -0
- package/lib/components/subModel/index.d.ts +10 -0
- package/lib/components/subModel/index.js +32 -0
- package/lib/components/subModel/utils.d.ts +34 -0
- package/lib/components/subModel/utils.js +287 -0
- package/lib/components/variables/InlineVariableTag.d.ts +21 -0
- package/lib/components/variables/InlineVariableTag.js +123 -0
- package/lib/components/variables/SlateVariableEditor.d.ts +46 -0
- package/lib/components/variables/SlateVariableEditor.js +302 -0
- package/lib/components/variables/VariableInput.d.ts +11 -0
- package/lib/components/variables/VariableInput.js +322 -0
- package/lib/components/variables/VariableTag.d.ts +11 -0
- package/lib/components/variables/VariableTag.js +148 -0
- package/lib/components/variables/VariableTrigger.d.ts +20 -0
- package/lib/components/variables/VariableTrigger.js +136 -0
- package/lib/components/variables/index.d.ts +15 -0
- package/lib/components/variables/index.js +53 -0
- package/lib/components/variables/types.d.ts +93 -0
- package/lib/components/variables/types.js +24 -0
- package/lib/components/variables/useResolvedMetaTree.d.ts +19 -0
- package/lib/components/variables/useResolvedMetaTree.js +91 -0
- package/lib/components/variables/utils.d.ts +22 -0
- package/lib/components/variables/utils.js +177 -0
- package/lib/data-source/index.d.ts +180 -0
- package/lib/data-source/index.js +733 -0
- package/lib/data-source/jioToJoiSchema.d.ts +19 -0
- package/lib/data-source/jioToJoiSchema.js +114 -0
- package/lib/decorators/index.d.ts +9 -0
- package/lib/decorators/index.js +36 -0
- package/lib/decorators/largeField.d.ts +9 -0
- package/lib/decorators/largeField.js +42 -0
- package/lib/emitter.d.ts +16 -0
- package/lib/emitter.js +58 -0
- package/lib/event-registry/BaseEventRegistry.d.ts +22 -0
- package/lib/event-registry/BaseEventRegistry.js +57 -0
- package/lib/event-registry/EngineEventRegistry.d.ts +19 -0
- package/lib/event-registry/EngineEventRegistry.js +47 -0
- package/lib/event-registry/ModelEventRegistry.d.ts +33 -0
- package/lib/event-registry/ModelEventRegistry.js +79 -0
- package/lib/executor/FlowExecutor.d.ts +26 -0
- package/lib/executor/FlowExecutor.js +262 -0
- package/lib/flow-registry/BaseFlowRegistry.d.ts +46 -0
- package/lib/flow-registry/BaseFlowRegistry.js +86 -0
- package/lib/flow-registry/GlobalFlowRegistry.d.ts +22 -0
- package/lib/flow-registry/GlobalFlowRegistry.js +95 -0
- package/lib/flow-registry/InstanceFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/InstanceFlowRegistry.js +59 -0
- package/lib/flow-registry/index.d.ts +11 -0
- package/lib/flow-registry/index.js +34 -0
- package/lib/flowContext.d.ts +215 -0
- package/lib/flowContext.js +1266 -0
- package/lib/flowEngine.d.ts +340 -0
- package/lib/flowEngine.js +781 -0
- package/lib/flowI18n.d.ts +46 -0
- package/lib/flowI18n.js +117 -0
- package/lib/flowSettings.d.ts +266 -0
- package/lib/flowSettings.js +850 -0
- package/lib/hooks/index.d.ts +14 -0
- package/lib/hooks/index.js +40 -0
- package/lib/hooks/useApplyAutoFlows.d.ts +21 -0
- package/lib/hooks/useApplyAutoFlows.js +62 -0
- package/lib/hooks/useFlowModel.d.ts +29 -0
- package/lib/hooks/useFlowModel.js +72 -0
- package/lib/hooks/useFlowModelById.d.ts +11 -0
- package/lib/hooks/useFlowModelById.js +61 -0
- package/lib/hooks/useFlowSettingsContext.d.ts +20 -0
- package/lib/hooks/useFlowSettingsContext.js +61 -0
- package/lib/hooks/useFlowStep.d.ts +17 -0
- package/lib/hooks/useFlowStep.js +56 -0
- package/lib/hooks/useNiceDropdownMaxHeight.d.ts +13 -0
- package/lib/hooks/useNiceDropdownMaxHeight.js +52 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +73 -0
- package/lib/locale/en-US.json +61 -0
- package/lib/locale/index.d.ts +141 -0
- package/lib/locale/index.js +70 -0
- package/lib/locale/zh-CN.json +61 -0
- package/lib/models/CollectionFieldModel.d.ts +50 -0
- package/lib/models/CollectionFieldModel.js +242 -0
- package/lib/models/DisplayItemModel.d.ts +12 -0
- package/lib/models/DisplayItemModel.js +41 -0
- package/lib/models/EditableItemModel.d.ts +12 -0
- package/lib/models/EditableItemModel.js +41 -0
- package/lib/models/FilterableItemModel.d.ts +12 -0
- package/lib/models/FilterableItemModel.js +41 -0
- package/lib/models/flowModel.d.ts +344 -0
- package/lib/models/flowModel.js +1133 -0
- package/lib/models/forkFlowModel.d.ts +83 -0
- package/lib/models/forkFlowModel.js +257 -0
- package/lib/models/index.d.ts +14 -0
- package/lib/models/index.js +40 -0
- package/lib/provider.d.ts +22 -0
- package/lib/provider.js +114 -0
- package/lib/resources/apiResource.d.ts +34 -0
- package/lib/resources/apiResource.js +153 -0
- package/lib/resources/baseRecordResource.d.ts +61 -0
- package/lib/resources/baseRecordResource.js +264 -0
- package/lib/resources/filterItem.d.ts +33 -0
- package/lib/resources/filterItem.js +93 -0
- package/lib/resources/flowResource.d.ts +45 -0
- package/lib/resources/flowResource.js +146 -0
- package/lib/resources/index.d.ts +15 -0
- package/lib/resources/index.js +42 -0
- package/lib/resources/multiRecordResource.d.ts +53 -0
- package/lib/resources/multiRecordResource.js +230 -0
- package/lib/resources/singleRecordResource.d.ts +23 -0
- package/lib/resources/singleRecordResource.js +111 -0
- package/lib/resources/sqlResource.d.ts +73 -0
- package/lib/resources/sqlResource.js +294 -0
- package/lib/runjs-context/contexts/FlowRunJSContext.d.ts +38 -0
- package/lib/runjs-context/contexts/FlowRunJSContext.js +217 -0
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +66 -0
- package/lib/runjs-context/contexts/JSBlockRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +78 -0
- package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.d.ts +12 -0
- package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.js +59 -0
- package/lib/runjs-context/contexts/JSFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +70 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +66 -0
- package/lib/runjs-context/contexts/JSRecordActionRunJSContext.d.ts +12 -0
- package/lib/runjs-context/contexts/JSRecordActionRunJSContext.js +61 -0
- package/lib/runjs-context/contexts/LinkageRunJSContext.d.ts +12 -0
- package/lib/runjs-context/contexts/LinkageRunJSContext.js +62 -0
- package/lib/runjs-context/helpers.d.ts +17 -0
- package/lib/runjs-context/helpers.js +79 -0
- package/lib/runjs-context/index.d.ts +19 -0
- package/lib/runjs-context/index.js +57 -0
- package/lib/runjs-context/registry.d.ts +17 -0
- package/lib/runjs-context/registry.js +93 -0
- package/lib/runjs-context/snippets/global/api-request-get.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/api-request-get.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/api-request-post.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/api-request-post.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/console-log-ctx.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/console-log-ctx.snippet.js +41 -0
- package/lib/runjs-context/snippets/global/copy-record-json.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/copy-record-json.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/log-json-record.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/log-json-record.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/message-error.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/message-error.snippet.js +41 -0
- package/lib/runjs-context/snippets/global/message-success.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/message-success.snippet.js +41 -0
- package/lib/runjs-context/snippets/global/notification-open.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/notification-open.snippet.js +43 -0
- package/lib/runjs-context/snippets/global/open-view-dialog.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/open-view-dialog.snippet.js +47 -0
- package/lib/runjs-context/snippets/global/open-view-drawer.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/open-view-drawer.snippet.js +47 -0
- package/lib/runjs-context/snippets/global/requireAsync.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/requireAsync.snippet.js +46 -0
- package/lib/runjs-context/snippets/global/sleep.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/sleep.snippet.js +43 -0
- package/lib/runjs-context/snippets/global/try-catch-async.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/try-catch-async.snippet.js +44 -0
- package/lib/runjs-context/snippets/global/view-navigation-push.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/view-navigation-push.snippet.js +44 -0
- package/lib/runjs-context/snippets/global/window-open.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/window-open.snippet.js +41 -0
- package/lib/runjs-context/snippets/index.d.ts +11 -0
- package/lib/runjs-context/snippets/index.js +94 -0
- package/lib/runjs-context/snippets/libs/echarts-init.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/libs/echarts-init.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.js +44 -0
- package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.js +43 -0
- package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.js +43 -0
- package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.js +40 -0
- package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.js +42 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.js +41 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.js +41 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.js +45 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.js +56 -0
- package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.js +42 -0
- package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.js +41 -0
- package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.js +40 -0
- package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.js +44 -0
- package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.js +60 -0
- package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.js +59 -0
- package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.js +60 -0
- package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.js +60 -0
- package/lib/runjs-context/snippets/types.d.ts +16 -0
- package/lib/runjs-context/snippets/types.js +24 -0
- package/lib/types.d.ts +398 -0
- package/lib/types.js +43 -0
- package/lib/utils/autoFlowError.d.ts +16 -0
- package/lib/utils/autoFlowError.js +53 -0
- package/lib/utils/constants.d.ts +29 -0
- package/lib/utils/constants.js +77 -0
- package/lib/utils/context.d.ts +40 -0
- package/lib/utils/context.js +63 -0
- package/lib/utils/createCollectionContextMeta.d.ts +11 -0
- package/lib/utils/createCollectionContextMeta.js +117 -0
- package/lib/utils/exceptions.d.ts +22 -0
- package/lib/utils/exceptions.js +62 -0
- package/lib/utils/flow-definitions.d.ts +11 -0
- package/lib/utils/flow-definitions.js +40 -0
- package/lib/utils/index.d.ts +22 -0
- package/lib/utils/index.js +117 -0
- package/lib/utils/inheritance.d.ts +16 -0
- package/lib/utils/inheritance.js +53 -0
- package/lib/utils/params-resolvers.d.ts +51 -0
- package/lib/utils/params-resolvers.js +309 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +34 -0
- package/lib/utils/parsePathnameToViewParams.js +84 -0
- package/lib/utils/safeGlobals.d.ts +16 -0
- package/lib/utils/safeGlobals.js +179 -0
- package/lib/utils/schema-utils.d.ts +40 -0
- package/lib/utils/schema-utils.js +161 -0
- package/lib/utils/serverContextParams.d.ts +28 -0
- package/lib/utils/serverContextParams.js +106 -0
- package/lib/utils/setupRuntimeContextSteps.d.ts +19 -0
- package/lib/utils/setupRuntimeContextSteps.js +88 -0
- package/lib/utils/translation.d.ts +18 -0
- package/lib/utils/translation.js +58 -0
- package/lib/utils/variablesParams.d.ts +51 -0
- package/lib/utils/variablesParams.js +150 -0
- package/lib/views/DialogComponent.d.ts +22 -0
- package/lib/views/DialogComponent.js +98 -0
- package/lib/views/DrawerComponent.d.ts +11 -0
- package/lib/views/DrawerComponent.js +101 -0
- package/lib/views/FlowView.d.ts +76 -0
- package/lib/views/FlowView.js +81 -0
- package/lib/views/PageComponent.d.ts +10 -0
- package/lib/views/PageComponent.js +167 -0
- package/lib/views/ViewNavigation.d.ts +45 -0
- package/lib/views/ViewNavigation.js +97 -0
- package/lib/views/createViewMeta.d.ts +16 -0
- package/lib/views/createViewMeta.js +171 -0
- package/lib/views/index.d.ts +13 -0
- package/lib/views/index.js +48 -0
- package/lib/views/useDialog.d.ts +32 -0
- package/lib/views/useDialog.js +199 -0
- package/lib/views/useDrawer.d.ts +33 -0
- package/lib/views/useDrawer.js +206 -0
- package/lib/views/usePage.d.ts +32 -0
- package/lib/views/usePage.js +193 -0
- package/lib/views/usePatchElement.d.ts +10 -0
- package/lib/views/usePatchElement.js +54 -0
- package/lib/views/usePopover.d.ts +17 -0
- package/lib/views/usePopover.js +159 -0
- package/package.json +37 -0
- package/src/ContextPathProxy.ts +45 -0
- package/src/ElementProxy.ts +69 -0
- package/src/FlowContextProvider.tsx +40 -0
- package/src/FlowDefinition.ts +275 -0
- package/src/JSRunner.ts +84 -0
- package/src/ReactView.tsx +104 -0
- package/src/ViewScopedFlowEngine.ts +75 -0
- package/src/__tests__/ElementProxy.test.ts +51 -0
- package/src/__tests__/JSRunner.test.ts +92 -0
- package/src/__tests__/ReactView.test.tsx +63 -0
- package/src/__tests__/context-path-proxy.test.ts +35 -0
- package/src/__tests__/flow-engine.test.ts +189 -0
- package/src/__tests__/flowContext.test.ts +2012 -0
- package/src/__tests__/flowEngine.saveModel.test.ts +171 -0
- package/src/__tests__/flowI18n.test.ts +28 -0
- package/src/__tests__/flowModel.getFlows.test.ts +61 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +78 -0
- package/src/__tests__/flowRuntimeContext.test.ts +187 -0
- package/src/__tests__/flowSettings.open.test.tsx +1920 -0
- package/src/__tests__/flowSettings.test.ts +566 -0
- package/src/__tests__/globalFlowRegistry.test.ts +77 -0
- package/src/__tests__/isFieldInterfaceMatch.test.ts +51 -0
- package/src/__tests__/metaTreeNodeCache.test.ts +234 -0
- package/src/__tests__/path-aggregation.test.ts +85 -0
- package/src/__tests__/provider.test.tsx +28 -0
- package/src/__tests__/renderHiddenInConfig.test.tsx +91 -0
- package/src/__tests__/runjsContext.test.ts +60 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +212 -0
- package/src/acl/Acl.tsx +109 -0
- package/src/acl/__tests__/Acl.test.tsx +72 -0
- package/src/action-registry/BaseActionRegistry.ts +46 -0
- package/src/action-registry/EngineActionRegistry.ts +32 -0
- package/src/action-registry/ModelActionRegistry.ts +75 -0
- package/src/action-registry/__tests__/engineActionRegistry.test.ts +43 -0
- package/src/action-registry/__tests__/modelActionRegistry.test.ts +107 -0
- package/src/components/DynamicFlowsEditor.tsx +318 -0
- package/src/components/FieldModelRenderer.tsx +62 -0
- package/src/components/FlowContextSelector.tsx +255 -0
- package/src/components/FlowErrorFallback.tsx +316 -0
- package/src/components/FlowModelRenderer.tsx +428 -0
- package/src/components/FormItem.tsx +130 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +226 -0
- package/src/components/common/FlowSettingsButton.tsx +30 -0
- package/src/components/common/index.ts +10 -0
- package/src/components/common/withFlowDesignMode.tsx +49 -0
- package/src/components/dnd/getMousePositionOnElement.ts +115 -0
- package/src/components/dnd/index.tsx +128 -0
- package/src/components/dnd/moveBlock.ts +379 -0
- package/src/components/index.ts +20 -0
- package/src/components/settings/independents/dropdown/FlowsDropdownButton.tsx +279 -0
- package/src/components/settings/independents/dropdown/index.ts +10 -0
- package/src/components/settings/independents/index.ts +2 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +617 -0
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +292 -0
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +655 -0
- package/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx +446 -0
- package/src/components/settings/wrappers/contextual/StepSettings.tsx +109 -0
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +217 -0
- package/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx +32 -0
- package/src/components/settings/wrappers/contextual/index.ts +15 -0
- package/src/components/settings/wrappers/embedded/FlowSettings.tsx +258 -0
- package/src/components/settings/wrappers/embedded/FlowsSettings.tsx +111 -0
- package/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx +96 -0
- package/src/components/settings/wrappers/embedded/index.ts +11 -0
- package/src/components/settings/wrappers/index.ts +5 -0
- package/src/components/subModel/AddSubModelButton.tsx +575 -0
- package/src/components/subModel/LazyDropdown.tsx +714 -0
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +1185 -0
- package/src/components/subModel/__tests__/buildWrapperFieldChildren.test.ts +192 -0
- package/src/components/subModel/__tests__/utils.test.ts +425 -0
- package/src/components/subModel/index.ts +12 -0
- package/src/components/subModel/utils.ts +278 -0
- package/src/components/variables/InlineVariableTag.tsx +97 -0
- package/src/components/variables/SlateVariableEditor.tsx +384 -0
- package/src/components/variables/VariableInput.tsx +342 -0
- package/src/components/variables/VariableTag.tsx +123 -0
- package/src/components/variables/VariableTrigger.tsx +116 -0
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +553 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +550 -0
- package/src/components/variables/__tests__/VariableTag.test.tsx +347 -0
- package/src/components/variables/__tests__/test-utils.tsx +62 -0
- package/src/components/variables/__tests__/utils.test.ts +310 -0
- package/src/components/variables/index.ts +16 -0
- package/src/components/variables/types.ts +100 -0
- package/src/components/variables/useResolvedMetaTree.ts +76 -0
- package/src/components/variables/utils.ts +192 -0
- package/src/data-source/__tests__/collection.test.ts +58 -0
- package/src/data-source/__tests__/index.test.ts +82 -0
- package/src/data-source/__tests__/jioToJoiSchema.test.ts +56 -0
- package/src/data-source/index.ts +812 -0
- package/src/data-source/jioToJoiSchema.ts +103 -0
- package/src/decorators/index.ts +10 -0
- package/src/decorators/largeField.ts +14 -0
- package/src/emitter.ts +33 -0
- package/src/event-registry/BaseEventRegistry.ts +40 -0
- package/src/event-registry/EngineEventRegistry.ts +26 -0
- package/src/event-registry/ModelEventRegistry.ts +69 -0
- package/src/event-registry/__tests__/engineEventRegistry.test.ts +48 -0
- package/src/executor/FlowExecutor.ts +256 -0
- package/src/executor/__tests__/eventStep.test.ts +157 -0
- package/src/executor/__tests__/flowExecutor.test.ts +163 -0
- package/src/flow-registry/BaseFlowRegistry.ts +91 -0
- package/src/flow-registry/GlobalFlowRegistry.ts +82 -0
- package/src/flow-registry/InstanceFlowRegistry.ts +39 -0
- package/src/flow-registry/__tests__/globalFlowRegistry.test.ts +141 -0
- package/src/flow-registry/__tests__/instance-and-global-registry.test.ts +67 -0
- package/src/flow-registry/__tests__/instanceFlowRegistry.test.ts +83 -0
- package/src/flow-registry/index.ts +12 -0
- package/src/flowContext.ts +1639 -0
- package/src/flowEngine.ts +905 -0
- package/src/flowI18n.ts +96 -0
- package/src/flowSettings.ts +1045 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useApplyAutoFlows.ts +51 -0
- package/src/hooks/useFlowModel.tsx +59 -0
- package/src/hooks/useFlowModelById.ts +37 -0
- package/src/hooks/useFlowSettingsContext.tsx +37 -0
- package/src/hooks/useFlowStep.tsx +19 -0
- package/src/hooks/useNiceDropdownMaxHeight.ts +34 -0
- package/src/index.ts +38 -0
- package/src/locale/en-US.json +61 -0
- package/src/locale/index.ts +38 -0
- package/src/locale/zh-CN.json +61 -0
- package/src/models/CollectionFieldModel.tsx +269 -0
- package/src/models/DisplayItemModel.tsx +13 -0
- package/src/models/EditableItemModel.tsx +13 -0
- package/src/models/FilterableItemModel.tsx +13 -0
- package/src/models/__tests__/CollectionFieldModel.test.ts +122 -0
- package/src/models/__tests__/defaultParams-on-create.test.ts +83 -0
- package/src/models/__tests__/flow-model-oninit.test.ts +44 -0
- package/src/models/__tests__/flowModel.actions.integration.test.ts +100 -0
- package/src/models/__tests__/flowModel.getFlows.sort.test.ts +100 -0
- package/src/models/__tests__/flowModel.test.ts +2746 -0
- package/src/models/__tests__/flowRegistry.test.ts +512 -0
- package/src/models/__tests__/forkFlowModel.test.ts +1047 -0
- package/src/models/__tests__/model-actions.test.ts +70 -0
- package/src/models/__tests__/model-events.test.ts +69 -0
- package/src/models/flowModel.tsx +1398 -0
- package/src/models/forkFlowModel.ts +287 -0
- package/src/models/index.ts +17 -0
- package/src/provider.tsx +101 -0
- package/src/resources/__tests__/apiResource.test.ts +201 -0
- package/src/resources/__tests__/baseRecordResource.test.ts +262 -0
- package/src/resources/__tests__/filterItem.test.ts +260 -0
- package/src/resources/__tests__/flowResource.test.ts +127 -0
- package/src/resources/apiResource.ts +148 -0
- package/src/resources/baseRecordResource.ts +279 -0
- package/src/resources/filterItem.ts +74 -0
- package/src/resources/flowResource.ts +143 -0
- package/src/resources/index.ts +17 -0
- package/src/resources/multiRecordResource.ts +219 -0
- package/src/resources/singleRecordResource.ts +83 -0
- package/src/resources/sqlResource.ts +299 -0
- package/src/runjs-context/contexts/FlowRunJSContext.ts +190 -0
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +39 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +52 -0
- package/src/runjs-context/contexts/JSCollectionActionRunJSContext.ts +32 -0
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +43 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +39 -0
- package/src/runjs-context/contexts/JSRecordActionRunJSContext.ts +34 -0
- package/src/runjs-context/contexts/LinkageRunJSContext.ts +35 -0
- package/src/runjs-context/helpers.ts +56 -0
- package/src/runjs-context/index.ts +20 -0
- package/src/runjs-context/registry.ts +65 -0
- package/src/runjs-context/snippets/global/api-request-get.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/api-request-post.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/console-log-ctx.snippet.ts +19 -0
- package/src/runjs-context/snippets/global/copy-record-json.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/copy-to-clipboard.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/log-json-record.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/message-error.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/message-success.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/notification-open.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/open-view-dialog.snippet.ts +26 -0
- package/src/runjs-context/snippets/global/open-view-drawer.snippet.ts +26 -0
- package/src/runjs-context/snippets/global/requireAsync.snippet.ts +24 -0
- package/src/runjs-context/snippets/global/sleep.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/try-catch-async.snippet.ts +22 -0
- package/src/runjs-context/snippets/global/view-navigation-push.snippet.ts +23 -0
- package/src/runjs-context/snippets/global/window-open.snippet.ts +19 -0
- package/src/runjs-context/snippets/index.ts +59 -0
- package/src/runjs-context/snippets/libs/echarts-init.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/actions/collection-selected-count.snippet.ts +22 -0
- package/src/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.ts +21 -0
- package/src/runjs-context/snippets/scene/actions/record-id-message.snippet.ts +21 -0
- package/src/runjs-context/snippets/scene/actions/run-action-basic.snippet.ts +18 -0
- package/src/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.ts +29 -0
- package/src/runjs-context/snippets/scene/jsblock/append-style.snippet.ts +20 -0
- package/src/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.ts +19 -0
- package/src/runjs-context/snippets/scene/jsblock/render-basic.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/jsblock/render-card.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/jsblock/render-react.snippet.ts +34 -0
- package/src/runjs-context/snippets/scene/jsfield/color-by-value.snippet.ts +20 -0
- package/src/runjs-context/snippets/scene/jsfield/format-number.snippet.ts +19 -0
- package/src/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.ts +18 -0
- package/src/runjs-context/snippets/scene/jsitem/render-basic.snippet.ts +27 -0
- package/src/runjs-context/snippets/scene/linkage/set-disabled.snippet.ts +38 -0
- package/src/runjs-context/snippets/scene/linkage/set-field-value.snippet.ts +37 -0
- package/src/runjs-context/snippets/scene/linkage/set-required.snippet.ts +38 -0
- package/src/runjs-context/snippets/scene/linkage/toggle-visible.snippet.ts +38 -0
- package/src/runjs-context/snippets/types.ts +17 -0
- package/src/types.ts +474 -0
- package/src/utils/__tests__/context.test.ts +93 -0
- package/src/utils/__tests__/params-resolvers.test.ts +652 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +104 -0
- package/src/utils/__tests__/safeGlobals.test.ts +29 -0
- package/src/utils/__tests__/utils.test.ts +1021 -0
- package/src/utils/__tests__/variablesParams.test.ts +52 -0
- package/src/utils/autoFlowError.ts +29 -0
- package/src/utils/constants.ts +60 -0
- package/src/utils/context.ts +70 -0
- package/src/utils/createCollectionContextMeta.ts +122 -0
- package/src/utils/exceptions.ts +36 -0
- package/src/utils/flow-definitions.ts +16 -0
- package/src/utils/index.ts +63 -0
- package/src/utils/inheritance.ts +39 -0
- package/src/utils/params-resolvers.ts +482 -0
- package/src/utils/parsePathnameToViewParams.ts +103 -0
- package/src/utils/safeGlobals.ts +188 -0
- package/src/utils/schema-utils.ts +201 -0
- package/src/utils/serverContextParams.ts +111 -0
- package/src/utils/setupRuntimeContextSteps.ts +89 -0
- package/src/utils/translation.ts +37 -0
- package/src/utils/variablesParams.ts +175 -0
- package/src/views/DialogComponent.tsx +79 -0
- package/src/views/DrawerComponent.tsx +72 -0
- package/src/views/FlowView.tsx +103 -0
- package/src/views/PageComponent.tsx +150 -0
- package/src/views/ViewNavigation.ts +122 -0
- package/src/views/__tests__/FlowView.test.ts +31 -0
- package/src/views/__tests__/ViewNavigation.test.ts +191 -0
- package/src/views/__tests__/usePatchElement.test.tsx +28 -0
- package/src/views/createViewMeta.ts +157 -0
- package/src/views/index.tsx +14 -0
- package/src/views/useDialog.tsx +192 -0
- package/src/views/useDrawer.tsx +205 -0
- package/src/views/usePage.tsx +182 -0
- package/src/views/usePatchElement.tsx +27 -0
- package/src/views/usePopover.tsx +131 -0
|
@@ -0,0 +1,1920 @@
|
|
|
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, it, expect, vi, afterEach } from 'vitest';
|
|
11
|
+
import { screen } from '@testing-library/react';
|
|
12
|
+
import { FlowSettings } from '../flowSettings';
|
|
13
|
+
import { FlowModel } from '../models';
|
|
14
|
+
import { FlowEngine } from '../flowEngine';
|
|
15
|
+
|
|
16
|
+
// We will stub viewer directly on model.context in tests
|
|
17
|
+
|
|
18
|
+
// Lightweight mocks for Formily/Antd components used during compile
|
|
19
|
+
vi.mock('@formily/antd-v5', () => ({
|
|
20
|
+
Form: () => 'Form',
|
|
21
|
+
FormDialog: () => 'FormDialog',
|
|
22
|
+
FormDrawer: () => 'FormDrawer',
|
|
23
|
+
FormItem: () => 'FormItem',
|
|
24
|
+
FormLayout: () => 'FormLayout',
|
|
25
|
+
FormGrid: () => 'FormGrid',
|
|
26
|
+
FormStep: () => 'FormStep',
|
|
27
|
+
FormTab: () => 'FormTab',
|
|
28
|
+
FormCollapse: () => 'FormCollapse',
|
|
29
|
+
FormButtonGroup: () => 'FormButtonGroup',
|
|
30
|
+
Input: () => 'Input',
|
|
31
|
+
NumberPicker: () => 'NumberPicker',
|
|
32
|
+
Password: () => 'Password',
|
|
33
|
+
Select: () => 'Select',
|
|
34
|
+
SelectTable: () => 'SelectTable',
|
|
35
|
+
Cascader: () => 'Cascader',
|
|
36
|
+
TreeSelect: () => 'TreeSelect',
|
|
37
|
+
Transfer: () => 'Transfer',
|
|
38
|
+
DatePicker: () => 'DatePicker',
|
|
39
|
+
TimePicker: () => 'TimePicker',
|
|
40
|
+
Checkbox: () => 'Checkbox',
|
|
41
|
+
Radio: () => 'Radio',
|
|
42
|
+
Switch: () => 'Switch',
|
|
43
|
+
ArrayBase: () => 'ArrayBase',
|
|
44
|
+
ArrayCards: () => 'ArrayCards',
|
|
45
|
+
ArrayCollapse: () => 'ArrayCollapse',
|
|
46
|
+
ArrayItems: () => 'ArrayItems',
|
|
47
|
+
ArrayTable: () => 'ArrayTable',
|
|
48
|
+
ArrayTabs: () => 'ArrayTabs',
|
|
49
|
+
Upload: () => 'Upload',
|
|
50
|
+
Space: () => 'Space',
|
|
51
|
+
Editable: () => 'Editable',
|
|
52
|
+
PreviewText: () => 'PreviewText',
|
|
53
|
+
Submit: () => 'Submit',
|
|
54
|
+
Reset: () => 'Reset',
|
|
55
|
+
}));
|
|
56
|
+
vi.mock('antd', () => {
|
|
57
|
+
const Collapse = Object.assign((props: any) => 'Collapse', { Panel: (props: any) => 'Panel' });
|
|
58
|
+
const FormItem = ({ children }: any) => children ?? 'FormItem';
|
|
59
|
+
const Form = Object.assign((props: any) => 'Form', {
|
|
60
|
+
Item: FormItem,
|
|
61
|
+
useForm: () => [{ setFieldsValue: (_: any) => {} }],
|
|
62
|
+
});
|
|
63
|
+
const Input: any = (props: any) => 'Input';
|
|
64
|
+
Input.TextArea = (props: any) => 'TextArea';
|
|
65
|
+
const InputNumber = (props: any) => 'InputNumber';
|
|
66
|
+
const Select = (props: any) => 'Select';
|
|
67
|
+
const Switch = (props: any) => 'Switch';
|
|
68
|
+
const Alert = (props: any) => 'Alert';
|
|
69
|
+
return {
|
|
70
|
+
Button: () => 'Button',
|
|
71
|
+
Space: () => 'Space',
|
|
72
|
+
Tabs: () => 'Tabs',
|
|
73
|
+
Collapse,
|
|
74
|
+
Result: () => 'Result',
|
|
75
|
+
Form,
|
|
76
|
+
Input,
|
|
77
|
+
InputNumber,
|
|
78
|
+
Select,
|
|
79
|
+
Switch,
|
|
80
|
+
Alert,
|
|
81
|
+
Typography: {
|
|
82
|
+
Paragraph: ({ children }: any) => children ?? 'Paragraph',
|
|
83
|
+
Text: ({ children }: any) => children ?? 'Text',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// helper to locate the primary Button element in a React element tree
|
|
89
|
+
// Updated to work with new Cancel/Save component structure
|
|
90
|
+
const findPrimaryButton = async (node: any) => {
|
|
91
|
+
const { Button } = await import('antd');
|
|
92
|
+
const walk = (n: any): any => {
|
|
93
|
+
if (!n || typeof n !== 'object') return null;
|
|
94
|
+
|
|
95
|
+
// Look for Button with type='primary'
|
|
96
|
+
if (n.type === Button && n?.props?.type === 'primary') return n;
|
|
97
|
+
|
|
98
|
+
// Also look for the Save component (function component that creates primary button)
|
|
99
|
+
if (typeof n.type === 'function' && n.type.name === 'Save') {
|
|
100
|
+
// Return the actual Button element created by Save component
|
|
101
|
+
const buttonEl = n.type(n.props);
|
|
102
|
+
if (buttonEl && buttonEl.type === Button && buttonEl.props?.type === 'primary') {
|
|
103
|
+
return { ...buttonEl, props: { ...buttonEl.props, onClick: n.props?.onClick || buttonEl.props?.onClick } };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const children = n.props?.children;
|
|
108
|
+
if (!children) return null;
|
|
109
|
+
if (Array.isArray(children)) {
|
|
110
|
+
for (const c of children) {
|
|
111
|
+
const r = walk(c);
|
|
112
|
+
if (r) return r;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return walk(children);
|
|
117
|
+
};
|
|
118
|
+
return walk(node);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// helper to locate the cancel Button (the non-primary one with text 'Cancel')
|
|
122
|
+
// Updated to work with new Cancel/Save component structure
|
|
123
|
+
const findCancelButton = async (node: any) => {
|
|
124
|
+
const { Button } = await import('antd');
|
|
125
|
+
const walk = (n: any): any => {
|
|
126
|
+
if (!n || typeof n !== 'object') return null;
|
|
127
|
+
|
|
128
|
+
// Look for Button without type and with 'Cancel' text
|
|
129
|
+
const isBtn = n.type === Button;
|
|
130
|
+
const isCancel = isBtn && !n?.props?.type && n?.props?.children === 'Cancel';
|
|
131
|
+
if (isCancel) return n;
|
|
132
|
+
|
|
133
|
+
// Also look for the Cancel component (function component that creates cancel button)
|
|
134
|
+
if (typeof n.type === 'function' && n.type.name === 'Cancel') {
|
|
135
|
+
// Return the actual Button element created by Cancel component
|
|
136
|
+
const buttonEl = n.type(n.props);
|
|
137
|
+
if (buttonEl && buttonEl.type === Button && !buttonEl.props?.type) {
|
|
138
|
+
return { ...buttonEl, props: { ...buttonEl.props, onClick: n.props?.onClick || buttonEl.props?.onClick } };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const children = n.props?.children;
|
|
143
|
+
if (!children) return null;
|
|
144
|
+
if (Array.isArray(children)) {
|
|
145
|
+
for (const c of children) {
|
|
146
|
+
const r = walk(c);
|
|
147
|
+
if (r) return r;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return walk(children);
|
|
152
|
+
};
|
|
153
|
+
return walk(node);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
describe('FlowSettings.open rendering behavior', () => {
|
|
157
|
+
afterEach(() => {
|
|
158
|
+
document.querySelectorAll('[data-testid]')?.forEach((n) => n.remove());
|
|
159
|
+
vi.clearAllMocks();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 创建测试特定的 FlowModel 子类,确保每个测试有独立的 globalFlowRegistry
|
|
163
|
+
const createIsolatedFlowModel = (testId: string) => {
|
|
164
|
+
// 动态创建匿名类,每个类都有独立的 globalFlowRegistry
|
|
165
|
+
return class extends FlowModel {
|
|
166
|
+
static testId = testId; // 用于调试识别
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
it('renders single-step form directly when flowKey+stepKey provided (no Collapse)', async () => {
|
|
171
|
+
const engine = new FlowEngine();
|
|
172
|
+
const flowSettings = new FlowSettings(engine);
|
|
173
|
+
const TestFlowModel = createIsolatedFlowModel('test-1');
|
|
174
|
+
const model = new TestFlowModel({ uid: 'm-open-1', flowEngine: engine });
|
|
175
|
+
|
|
176
|
+
// Register a dummy flow with one step
|
|
177
|
+
TestFlowModel.registerFlow({
|
|
178
|
+
key: 'testFlow',
|
|
179
|
+
steps: {
|
|
180
|
+
general: {
|
|
181
|
+
title: 'General',
|
|
182
|
+
uiSchema: {
|
|
183
|
+
field1: { type: 'string', 'x-component': 'Input' },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// stub viewer
|
|
190
|
+
model.context.defineProperty('viewer', {
|
|
191
|
+
value: {
|
|
192
|
+
dialog: ({ content }) => {
|
|
193
|
+
const dialog = {
|
|
194
|
+
close: () => container.remove(),
|
|
195
|
+
Footer: () => null,
|
|
196
|
+
} as any;
|
|
197
|
+
const container = document.createElement('div');
|
|
198
|
+
container.setAttribute('data-testid', 'flow-settings-container');
|
|
199
|
+
// execute content function to ensure render path doesn't throw
|
|
200
|
+
if (typeof content === 'function') {
|
|
201
|
+
content(dialog, { defineMethod: vi.fn() });
|
|
202
|
+
}
|
|
203
|
+
document.body.appendChild(container);
|
|
204
|
+
return dialog;
|
|
205
|
+
},
|
|
206
|
+
drawer: ({ content }) => {
|
|
207
|
+
const dialog = {
|
|
208
|
+
close: () => container.remove?.(),
|
|
209
|
+
Footer: () => null,
|
|
210
|
+
} as any;
|
|
211
|
+
const container = document.createElement('div');
|
|
212
|
+
container.setAttribute('data-testid', 'flow-settings-container');
|
|
213
|
+
if (typeof content === 'function') {
|
|
214
|
+
content(dialog, { defineMethod: vi.fn() });
|
|
215
|
+
}
|
|
216
|
+
document.body.appendChild(container);
|
|
217
|
+
return dialog;
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await flowSettings.open({ model, flowKey: 'testFlow', stepKey: 'general', uiMode: 'dialog' } as any);
|
|
223
|
+
|
|
224
|
+
const container = screen.getByTestId('flow-settings-container');
|
|
225
|
+
expect(container).toBeInTheDocument();
|
|
226
|
+
// Should NOT render Collapse markers since we render a plain form
|
|
227
|
+
expect(container.querySelector('.ant-collapse')).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('renders Collapse when only one step matched but no stepKey provided', async () => {
|
|
231
|
+
const engine = new FlowEngine();
|
|
232
|
+
const flowSettings = new FlowSettings(engine);
|
|
233
|
+
const TestFlowModel = createIsolatedFlowModel('test-2');
|
|
234
|
+
const model = new TestFlowModel({ uid: 'm-open-2', flowEngine: engine });
|
|
235
|
+
|
|
236
|
+
TestFlowModel.registerFlow({
|
|
237
|
+
key: 'testFlow2',
|
|
238
|
+
steps: {
|
|
239
|
+
general: {
|
|
240
|
+
title: 'General',
|
|
241
|
+
uiSchema: {
|
|
242
|
+
field1: { type: 'string', 'x-component': 'Input' },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// stub viewer
|
|
249
|
+
model.context.defineProperty('viewer', {
|
|
250
|
+
value: {
|
|
251
|
+
dialog: ({ content }) => {
|
|
252
|
+
const dialog = {
|
|
253
|
+
close: () => container.remove(),
|
|
254
|
+
Footer: () => null,
|
|
255
|
+
} as any;
|
|
256
|
+
const container = document.createElement('div');
|
|
257
|
+
container.setAttribute('data-testid', 'flow-settings-container');
|
|
258
|
+
if (typeof content === 'function') {
|
|
259
|
+
content(dialog, { defineMethod: vi.fn() });
|
|
260
|
+
}
|
|
261
|
+
document.body.appendChild(container);
|
|
262
|
+
return dialog;
|
|
263
|
+
},
|
|
264
|
+
drawer: ({ content }) => {
|
|
265
|
+
const dialog = {
|
|
266
|
+
close: () => container.remove?.(),
|
|
267
|
+
Footer: () => null,
|
|
268
|
+
} as any;
|
|
269
|
+
const container = document.createElement('div');
|
|
270
|
+
container.setAttribute('data-testid', 'flow-settings-container');
|
|
271
|
+
if (typeof content === 'function') {
|
|
272
|
+
content(dialog, { defineMethod: vi.fn() });
|
|
273
|
+
}
|
|
274
|
+
document.body.appendChild(container);
|
|
275
|
+
return dialog;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await flowSettings.open({ model, flowKey: 'testFlow2', uiMode: 'dialog' } as any);
|
|
281
|
+
|
|
282
|
+
const container = screen.getByTestId('flow-settings-container');
|
|
283
|
+
expect(container).toBeInTheDocument();
|
|
284
|
+
// In our minimal mock we don't render real Collapse DOM, but we can check that the FlowViewer was invoked.
|
|
285
|
+
// For robustness, just assert container exists (behavior difference is ensured by code path and not by DOM markers here).
|
|
286
|
+
expect(container).toBeTruthy();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('shows info when there are no configurable steps (entries length 0)', async () => {
|
|
290
|
+
const engine = new FlowEngine();
|
|
291
|
+
const flowSettings = new FlowSettings(engine);
|
|
292
|
+
const TestFlowModel = createIsolatedFlowModel('test-3');
|
|
293
|
+
const model = new TestFlowModel({ uid: 'm-open-none', flowEngine: engine });
|
|
294
|
+
|
|
295
|
+
// message spy
|
|
296
|
+
const info = vi.fn();
|
|
297
|
+
const error = vi.fn();
|
|
298
|
+
const success = vi.fn();
|
|
299
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
300
|
+
|
|
301
|
+
// viewer spies (should NOT be called)
|
|
302
|
+
const dialog = vi.fn();
|
|
303
|
+
const drawer = vi.fn();
|
|
304
|
+
model.context.defineProperty('viewer', { value: { dialog, drawer } });
|
|
305
|
+
|
|
306
|
+
await flowSettings.open({ model } as any);
|
|
307
|
+
|
|
308
|
+
expect(info).toHaveBeenCalled();
|
|
309
|
+
expect(dialog).not.toHaveBeenCalled();
|
|
310
|
+
expect(drawer).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('uses drawer uiMode when specified', async () => {
|
|
314
|
+
const engine = new FlowEngine();
|
|
315
|
+
const flowSettings = new FlowSettings(engine);
|
|
316
|
+
const TestFlowModel = createIsolatedFlowModel('test-4');
|
|
317
|
+
const model = new TestFlowModel({ uid: 'm-open-drawer', flowEngine: engine });
|
|
318
|
+
|
|
319
|
+
// Register one simple flow/step
|
|
320
|
+
TestFlowModel.registerFlow({
|
|
321
|
+
key: 'f',
|
|
322
|
+
steps: {
|
|
323
|
+
s: {
|
|
324
|
+
title: 'S',
|
|
325
|
+
uiSchema: {
|
|
326
|
+
field: { type: 'string', 'x-component': 'Input' },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// message stub
|
|
333
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
334
|
+
|
|
335
|
+
const openSpy = { lastTree: null as any };
|
|
336
|
+
const viewerDrawer = vi.fn(({ content }) => {
|
|
337
|
+
const dlg = { close: vi.fn(), Footer: (props: any) => null } as any;
|
|
338
|
+
openSpy.lastTree = typeof content === 'function' ? content(dlg, { defineMethod: vi.fn() }) : null;
|
|
339
|
+
return dlg;
|
|
340
|
+
});
|
|
341
|
+
const viewerDialog = vi.fn();
|
|
342
|
+
model.context.defineProperty('viewer', { value: { drawer: viewerDrawer, dialog: viewerDialog } });
|
|
343
|
+
|
|
344
|
+
await flowSettings.open({ model, flowKey: 'f', uiMode: 'drawer' } as any);
|
|
345
|
+
expect(viewerDrawer).toHaveBeenCalledTimes(1);
|
|
346
|
+
expect(viewerDialog).not.toHaveBeenCalled();
|
|
347
|
+
expect(openSpy.lastTree).toBeTruthy();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('saves successfully and calls hooks, messages, and close', async () => {
|
|
351
|
+
const engine = new FlowEngine();
|
|
352
|
+
const flowSettings = new FlowSettings(engine);
|
|
353
|
+
const TestFlowModel = createIsolatedFlowModel('test-5');
|
|
354
|
+
const model = new TestFlowModel({ uid: 'm-open-save', flowEngine: engine });
|
|
355
|
+
|
|
356
|
+
const beforeHook = vi.fn();
|
|
357
|
+
const afterHook = vi.fn();
|
|
358
|
+
TestFlowModel.registerFlow({
|
|
359
|
+
key: 'fx',
|
|
360
|
+
steps: {
|
|
361
|
+
general: {
|
|
362
|
+
title: 'General',
|
|
363
|
+
defaultParams: { a: 1 },
|
|
364
|
+
beforeParamsSave: beforeHook,
|
|
365
|
+
afterParamsSave: afterHook,
|
|
366
|
+
uiSchema: {
|
|
367
|
+
field1: { type: 'string', 'x-component': 'Input' },
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const info = vi.fn();
|
|
374
|
+
const error = vi.fn();
|
|
375
|
+
const success = vi.fn();
|
|
376
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
377
|
+
|
|
378
|
+
const setStepParams = vi.spyOn(model as any, 'setStepParams');
|
|
379
|
+
const save = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
|
|
380
|
+
|
|
381
|
+
// capture returned React tree for triggering primary button
|
|
382
|
+
let lastTree: any;
|
|
383
|
+
let lastDialog: any;
|
|
384
|
+
model.context.defineProperty('viewer', {
|
|
385
|
+
value: {
|
|
386
|
+
dialog: ({ content }) => {
|
|
387
|
+
lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
388
|
+
lastTree = typeof content === 'function' ? content(lastDialog) : null;
|
|
389
|
+
// return dialog object
|
|
390
|
+
return lastDialog;
|
|
391
|
+
},
|
|
392
|
+
drawer: ({ content }) => (model as any).context.viewer.dialog({ content }),
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
await flowSettings.open({ model, flowKey: 'fx', stepKey: 'general', uiMode: 'dialog' } as any);
|
|
397
|
+
|
|
398
|
+
// traverse the created React element tree to find the primary Button and click it
|
|
399
|
+
const primaryBtn = await findPrimaryButton(lastTree);
|
|
400
|
+
expect(primaryBtn).toBeTruthy();
|
|
401
|
+
await primaryBtn.props.onClick?.();
|
|
402
|
+
|
|
403
|
+
// assertions
|
|
404
|
+
expect(setStepParams).toHaveBeenCalledWith('fx', 'general', expect.any(Object));
|
|
405
|
+
expect(beforeHook).toHaveBeenCalled();
|
|
406
|
+
expect(save).toHaveBeenCalled();
|
|
407
|
+
expect(success).toHaveBeenCalled();
|
|
408
|
+
expect(afterHook).toHaveBeenCalled();
|
|
409
|
+
expect(lastDialog.close).toHaveBeenCalled();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('calls onSaved callback after successful save', async () => {
|
|
413
|
+
const engine = new FlowEngine();
|
|
414
|
+
const flowSettings = new FlowSettings(engine);
|
|
415
|
+
const TestFlowModel = createIsolatedFlowModel('test-6');
|
|
416
|
+
const model = new TestFlowModel({ uid: 'm-open-onsaved', flowEngine: engine });
|
|
417
|
+
|
|
418
|
+
TestFlowModel.registerFlow({
|
|
419
|
+
key: 'flow',
|
|
420
|
+
steps: {
|
|
421
|
+
step: {
|
|
422
|
+
title: 'Step',
|
|
423
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
429
|
+
vi.spyOn(model as any, 'save').mockResolvedValue(undefined);
|
|
430
|
+
|
|
431
|
+
let lastTree: any;
|
|
432
|
+
let lastDialog: any;
|
|
433
|
+
model.context.defineProperty('viewer', {
|
|
434
|
+
value: {
|
|
435
|
+
dialog: ({ content }) => {
|
|
436
|
+
lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
437
|
+
lastTree = typeof content === 'function' ? content(lastDialog) : null;
|
|
438
|
+
return lastDialog;
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const onSaved = vi.fn();
|
|
444
|
+
await flowSettings.open({ model, flowKey: 'flow', stepKey: 'step', onSaved } as any);
|
|
445
|
+
|
|
446
|
+
const primaryBtn = await findPrimaryButton(lastTree);
|
|
447
|
+
expect(primaryBtn).toBeTruthy();
|
|
448
|
+
await primaryBtn.props.onClick?.();
|
|
449
|
+
|
|
450
|
+
expect(onSaved).toHaveBeenCalledTimes(1);
|
|
451
|
+
expect(lastDialog.close).toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('calls onCancel callback when cancel button clicked', async () => {
|
|
455
|
+
const engine = new FlowEngine();
|
|
456
|
+
const flowSettings = new FlowSettings(engine);
|
|
457
|
+
const TestFlowModel = createIsolatedFlowModel('test-7');
|
|
458
|
+
const model = new TestFlowModel({ uid: 'm-open-oncancel', flowEngine: engine });
|
|
459
|
+
|
|
460
|
+
TestFlowModel.registerFlow({
|
|
461
|
+
key: 'flow',
|
|
462
|
+
steps: {
|
|
463
|
+
step: {
|
|
464
|
+
title: 'Step',
|
|
465
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
471
|
+
|
|
472
|
+
let lastTree: any;
|
|
473
|
+
let lastDialog: any;
|
|
474
|
+
model.context.defineProperty('viewer', {
|
|
475
|
+
value: {
|
|
476
|
+
dialog: ({ content }) => {
|
|
477
|
+
lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
478
|
+
lastTree = typeof content === 'function' ? content(lastDialog) : null;
|
|
479
|
+
return lastDialog;
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const onCancel = vi.fn();
|
|
485
|
+
await flowSettings.open({ model, flowKey: 'flow', stepKey: 'step', onCancel } as any);
|
|
486
|
+
|
|
487
|
+
const cancelBtn = await findCancelButton(lastTree);
|
|
488
|
+
expect(cancelBtn).toBeTruthy();
|
|
489
|
+
await cancelBtn.props.onClick?.();
|
|
490
|
+
|
|
491
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
492
|
+
expect(lastDialog.close).toHaveBeenCalled();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('handles save error by showing error message and keeping dialog open', async () => {
|
|
496
|
+
const engine = new FlowEngine();
|
|
497
|
+
const flowSettings = new FlowSettings(engine);
|
|
498
|
+
const TestFlowModel = createIsolatedFlowModel('test-8');
|
|
499
|
+
const model = new TestFlowModel({ uid: 'm-open-error', flowEngine: engine });
|
|
500
|
+
|
|
501
|
+
TestFlowModel.registerFlow({
|
|
502
|
+
key: 'fy',
|
|
503
|
+
steps: {
|
|
504
|
+
s: {
|
|
505
|
+
title: 'S',
|
|
506
|
+
defaultParams: { b: 2 },
|
|
507
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const info = vi.fn();
|
|
513
|
+
const error = vi.fn();
|
|
514
|
+
const success = vi.fn();
|
|
515
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
516
|
+
|
|
517
|
+
vi.spyOn(model as any, 'saveStepParams').mockRejectedValue(new Error('boom'));
|
|
518
|
+
|
|
519
|
+
let lastTree: any;
|
|
520
|
+
let lastDialog: any;
|
|
521
|
+
model.context.defineProperty('viewer', {
|
|
522
|
+
value: {
|
|
523
|
+
dialog: ({ content }) => {
|
|
524
|
+
lastDialog = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
525
|
+
lastTree = typeof content === 'function' ? content(lastDialog) : null;
|
|
526
|
+
return lastDialog;
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
await flowSettings.open({ model, flowKey: 'fy', stepKey: 's' } as any);
|
|
532
|
+
|
|
533
|
+
const primaryBtn = await findPrimaryButton(lastTree);
|
|
534
|
+
expect(primaryBtn).toBeTruthy();
|
|
535
|
+
await primaryBtn.props.onClick?.();
|
|
536
|
+
|
|
537
|
+
expect(error).toHaveBeenCalled();
|
|
538
|
+
expect(success).not.toHaveBeenCalled();
|
|
539
|
+
expect(lastDialog.close).not.toHaveBeenCalled();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('filters steps by preset when preset=true', async () => {
|
|
543
|
+
const engine = new FlowEngine();
|
|
544
|
+
const flowSettings = new FlowSettings(engine);
|
|
545
|
+
const TestFlowModel = createIsolatedFlowModel('test-9');
|
|
546
|
+
const model = new TestFlowModel({ uid: 'm-open-preset', flowEngine: engine });
|
|
547
|
+
|
|
548
|
+
TestFlowModel.registerFlow({
|
|
549
|
+
key: 'pf',
|
|
550
|
+
steps: {
|
|
551
|
+
a: {
|
|
552
|
+
title: 'A',
|
|
553
|
+
preset: true,
|
|
554
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
555
|
+
},
|
|
556
|
+
b: {
|
|
557
|
+
title: 'B',
|
|
558
|
+
uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
564
|
+
const dialog = vi.fn(({ content }) => {
|
|
565
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
566
|
+
if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
|
|
567
|
+
return dlg;
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
model.context.defineProperty('viewer', {
|
|
571
|
+
value: {
|
|
572
|
+
dialog,
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
await flowSettings.open({ model, flowKey: 'pf', preset: true } as any);
|
|
577
|
+
expect(dialog).toHaveBeenCalled();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('shows info when preset=true but no step is preset', async () => {
|
|
581
|
+
const engine = new FlowEngine();
|
|
582
|
+
const flowSettings = new FlowSettings(engine);
|
|
583
|
+
const TestFlowModel = createIsolatedFlowModel('test-10');
|
|
584
|
+
const model = new TestFlowModel({ uid: 'm-open-preset-none', flowEngine: engine });
|
|
585
|
+
|
|
586
|
+
TestFlowModel.registerFlow({
|
|
587
|
+
key: 'pf2',
|
|
588
|
+
steps: {
|
|
589
|
+
x: { title: 'X', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } },
|
|
590
|
+
y: { title: 'Y', uiSchema: { g: { type: 'string', 'x-component': 'Input' } } },
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const info = vi.fn();
|
|
595
|
+
const dialog = vi.fn();
|
|
596
|
+
model.context.defineProperty('message', { value: { info, error: vi.fn(), success: vi.fn() } });
|
|
597
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
598
|
+
|
|
599
|
+
await flowSettings.open({ model, flowKey: 'pf2', preset: true } as any);
|
|
600
|
+
expect(info).not.toHaveBeenCalled(); // 这种一般是在添加 sub model 的场景调用的,如果为空应该直接忽略,不需要 info 提示
|
|
601
|
+
expect(dialog).not.toHaveBeenCalled();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('shows dialog when preset=true and step has hideInSettings=true', async () => {
|
|
605
|
+
const engine = new FlowEngine();
|
|
606
|
+
const flowSettings = new FlowSettings(engine);
|
|
607
|
+
const model = new FlowModel({ uid: 'm-open-preset-hidden', flowEngine: engine });
|
|
608
|
+
|
|
609
|
+
const M = model.constructor as any;
|
|
610
|
+
M.registerFlow({
|
|
611
|
+
key: 'pf3',
|
|
612
|
+
steps: {
|
|
613
|
+
hiddenStep: {
|
|
614
|
+
title: 'Hidden Step',
|
|
615
|
+
preset: true,
|
|
616
|
+
hideInSettings: true,
|
|
617
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
618
|
+
},
|
|
619
|
+
visibleStep: {
|
|
620
|
+
title: 'Visible Step',
|
|
621
|
+
uiSchema: { field2: { type: 'string', 'x-component': 'Input' } },
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
627
|
+
const dialog = vi.fn(({ content }) => {
|
|
628
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
629
|
+
if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
|
|
630
|
+
return dlg;
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
634
|
+
|
|
635
|
+
await flowSettings.open({ model, flowKey: 'pf3', preset: true } as any);
|
|
636
|
+
expect(dialog).toHaveBeenCalled(); // 应该显示弹窗,因为 hiddenStep 有 preset=true
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('ignores hideInSettings when preset=true for individual step', async () => {
|
|
640
|
+
const engine = new FlowEngine();
|
|
641
|
+
const flowSettings = new FlowSettings(engine);
|
|
642
|
+
const model = new FlowModel({ uid: 'm-open-preset-single-hidden', flowEngine: engine });
|
|
643
|
+
|
|
644
|
+
const M = model.constructor as any;
|
|
645
|
+
M.registerFlow({
|
|
646
|
+
key: 'pf4',
|
|
647
|
+
steps: {
|
|
648
|
+
targetStep: {
|
|
649
|
+
title: 'Target Step',
|
|
650
|
+
preset: true,
|
|
651
|
+
hideInSettings: true,
|
|
652
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
658
|
+
const dialog = vi.fn(({ content }) => {
|
|
659
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
660
|
+
if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
|
|
661
|
+
return dlg;
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
665
|
+
|
|
666
|
+
await flowSettings.open({
|
|
667
|
+
model,
|
|
668
|
+
flowKey: 'pf4',
|
|
669
|
+
stepKey: 'targetStep',
|
|
670
|
+
preset: true,
|
|
671
|
+
} as any);
|
|
672
|
+
|
|
673
|
+
expect(dialog).toHaveBeenCalled(); // 应该显示弹窗,即使 hideInSettings=true
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('respects hideInSettings when preset=false', async () => {
|
|
677
|
+
const engine = new FlowEngine();
|
|
678
|
+
const flowSettings = new FlowSettings(engine);
|
|
679
|
+
const model = new FlowModel({ uid: 'm-open-normal-hidden', flowEngine: engine });
|
|
680
|
+
|
|
681
|
+
const M = model.constructor as any;
|
|
682
|
+
M.registerFlow({
|
|
683
|
+
key: 'pf5',
|
|
684
|
+
steps: {
|
|
685
|
+
hiddenStep: {
|
|
686
|
+
title: 'Hidden Step',
|
|
687
|
+
hideInSettings: true,
|
|
688
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
689
|
+
},
|
|
690
|
+
visibleStep: {
|
|
691
|
+
title: 'Visible Step',
|
|
692
|
+
uiSchema: { field2: { type: 'string', 'x-component': 'Input' } },
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
698
|
+
const dialog = vi.fn(({ content }) => {
|
|
699
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
700
|
+
if (typeof content === 'function') content(dlg, { defineMethod: vi.fn() });
|
|
701
|
+
return dlg;
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
705
|
+
|
|
706
|
+
await flowSettings.open({ model, flowKey: 'pf5', preset: false } as any);
|
|
707
|
+
expect(dialog).toHaveBeenCalled(); // 应该显示弹窗,但只包含 visibleStep
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('accepts uiMode object (dialog) and merges props while keeping our content', async () => {
|
|
711
|
+
const engine = new FlowEngine();
|
|
712
|
+
const flowSettings = new FlowSettings(engine);
|
|
713
|
+
const TestFlowModel = createIsolatedFlowModel('test-11');
|
|
714
|
+
const model = new TestFlowModel({ uid: 'm-open-uiMode-obj-dialog', flowEngine: engine });
|
|
715
|
+
|
|
716
|
+
TestFlowModel.registerFlow({
|
|
717
|
+
key: 'flowObj',
|
|
718
|
+
steps: {
|
|
719
|
+
step: {
|
|
720
|
+
title: 'Step',
|
|
721
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const info = vi.fn();
|
|
727
|
+
const error = vi.fn();
|
|
728
|
+
const success = vi.fn();
|
|
729
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
730
|
+
|
|
731
|
+
const sentinelContent = vi.fn();
|
|
732
|
+
const viewerDialog = vi.fn((opts: any) => {
|
|
733
|
+
// title/width should come from props
|
|
734
|
+
expect(opts.title).toBe('Custom title');
|
|
735
|
+
expect(opts.width).toBe(1024);
|
|
736
|
+
// extra props should pass through
|
|
737
|
+
expect(opts.maskClosable).toBe(false);
|
|
738
|
+
// our content should override incoming props.content
|
|
739
|
+
expect(opts.content).not.toBe(sentinelContent);
|
|
740
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
741
|
+
// Execute content once to ensure it is callable
|
|
742
|
+
if (typeof opts.content === 'function') {
|
|
743
|
+
opts.content(dlg, { defineMethod: vi.fn() });
|
|
744
|
+
}
|
|
745
|
+
return dlg;
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
model.context.defineProperty('viewer', { value: { dialog: viewerDialog } });
|
|
749
|
+
|
|
750
|
+
await flowSettings.open({
|
|
751
|
+
model,
|
|
752
|
+
flowKey: 'flowObj',
|
|
753
|
+
stepKey: 'step',
|
|
754
|
+
uiMode: {
|
|
755
|
+
type: 'dialog',
|
|
756
|
+
props: { title: 'Custom title', width: 1024, maskClosable: false, content: sentinelContent },
|
|
757
|
+
},
|
|
758
|
+
} as any);
|
|
759
|
+
|
|
760
|
+
expect(viewerDialog).toHaveBeenCalledTimes(1);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('accepts uiMode object with type drawer and calls viewer.drawer', async () => {
|
|
764
|
+
const engine = new FlowEngine();
|
|
765
|
+
const flowSettings = new FlowSettings(engine);
|
|
766
|
+
const TestFlowModel = createIsolatedFlowModel('test-12');
|
|
767
|
+
const model = new TestFlowModel({ uid: 'm-open-uiMode-obj-drawer', flowEngine: engine });
|
|
768
|
+
|
|
769
|
+
TestFlowModel.registerFlow({
|
|
770
|
+
key: 'flowObj2',
|
|
771
|
+
steps: {
|
|
772
|
+
step: {
|
|
773
|
+
title: 'Step',
|
|
774
|
+
uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const drawer = vi.fn((opts: any) => {
|
|
780
|
+
// also check title fallback if not provided in props
|
|
781
|
+
expect(typeof opts.title === 'string').toBe(true);
|
|
782
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
783
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
784
|
+
return dlg;
|
|
785
|
+
});
|
|
786
|
+
const dialog = vi.fn();
|
|
787
|
+
|
|
788
|
+
model.context.defineProperty('viewer', { value: { drawer, dialog } });
|
|
789
|
+
|
|
790
|
+
await flowSettings.open({
|
|
791
|
+
model,
|
|
792
|
+
flowKey: 'flowObj2',
|
|
793
|
+
stepKey: 'step',
|
|
794
|
+
uiMode: { type: 'drawer', props: {} },
|
|
795
|
+
} as any);
|
|
796
|
+
|
|
797
|
+
expect(drawer).toHaveBeenCalledTimes(1);
|
|
798
|
+
expect(dialog).not.toHaveBeenCalled();
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('sets title to step title when flowKey+stepKey are provided and only one step matches', async () => {
|
|
802
|
+
const engine = new FlowEngine();
|
|
803
|
+
const flowSettings = new FlowSettings(engine);
|
|
804
|
+
const TestFlowModel = createIsolatedFlowModel('test-13');
|
|
805
|
+
const model = new TestFlowModel({ uid: 'm-open-title-step', flowEngine: engine });
|
|
806
|
+
|
|
807
|
+
TestFlowModel.registerFlow({
|
|
808
|
+
key: 'tf-step',
|
|
809
|
+
steps: {
|
|
810
|
+
general: {
|
|
811
|
+
title: 'General',
|
|
812
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const dialog = vi.fn((opts: any) => {
|
|
818
|
+
expect(opts.title).toBe('General');
|
|
819
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
820
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
821
|
+
return dlg;
|
|
822
|
+
});
|
|
823
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
824
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
825
|
+
|
|
826
|
+
await flowSettings.open({ model, flowKey: 'tf-step', stepKey: 'general' } as any);
|
|
827
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('sets title to flow title when only one flow and no stepKey', async () => {
|
|
831
|
+
const engine = new FlowEngine();
|
|
832
|
+
const flowSettings = new FlowSettings(engine);
|
|
833
|
+
const TestFlowModel = createIsolatedFlowModel('test-14');
|
|
834
|
+
const model = new TestFlowModel({ uid: 'm-open-title-flow', flowEngine: engine });
|
|
835
|
+
|
|
836
|
+
TestFlowModel.registerFlow({
|
|
837
|
+
key: 'tf-flow',
|
|
838
|
+
title: 'My Flow',
|
|
839
|
+
steps: {
|
|
840
|
+
a: { title: 'A', uiSchema: { f: { type: 'string', 'x-component': 'Input' } } },
|
|
841
|
+
b: { title: 'B', uiSchema: { g: { type: 'string', 'x-component': 'Input' } } },
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const dialog = vi.fn((opts: any) => {
|
|
846
|
+
expect(opts.title).toBe('A');
|
|
847
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
848
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
849
|
+
return dlg;
|
|
850
|
+
});
|
|
851
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
852
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
853
|
+
|
|
854
|
+
// explicitly pass flowKey to avoid interference from other tests
|
|
855
|
+
await flowSettings.open({ model, flowKey: 'tf-flow' } as any);
|
|
856
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('sets empty title when rendering multiple flows together', async () => {
|
|
860
|
+
const engine = new FlowEngine();
|
|
861
|
+
const flowSettings = new FlowSettings(engine);
|
|
862
|
+
const TestFlowModel = createIsolatedFlowModel('test-15');
|
|
863
|
+
const model = new TestFlowModel({ uid: 'm-open-title-empty', flowEngine: engine });
|
|
864
|
+
|
|
865
|
+
TestFlowModel.registerFlow({
|
|
866
|
+
key: 'f1',
|
|
867
|
+
title: 'Flow 1',
|
|
868
|
+
steps: { s1: { title: 'S1', uiSchema: { a: { type: 'string', 'x-component': 'Input' } } } },
|
|
869
|
+
});
|
|
870
|
+
TestFlowModel.registerFlow({
|
|
871
|
+
key: 'f2',
|
|
872
|
+
title: 'Flow 2',
|
|
873
|
+
steps: { s2: { title: 'S2', uiSchema: { b: { type: 'string', 'x-component': 'Input' } } } },
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
const dialog = vi.fn((opts: any) => {
|
|
877
|
+
expect(opts.title).toBe('');
|
|
878
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
879
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
880
|
+
return dlg;
|
|
881
|
+
});
|
|
882
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
883
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
884
|
+
|
|
885
|
+
// omit flowKey to include all flows
|
|
886
|
+
await flowSettings.open({ model } as any);
|
|
887
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('resolves function-based step uiMode when single step is rendered', async () => {
|
|
891
|
+
const engine = new FlowEngine();
|
|
892
|
+
const flowSettings = new FlowSettings(engine);
|
|
893
|
+
const model = new FlowModel({ uid: 'm-step-uimode-function', flowEngine: engine });
|
|
894
|
+
|
|
895
|
+
const M = model.constructor as any;
|
|
896
|
+
M.registerFlow({
|
|
897
|
+
key: 'flowWithFunctionUiMode',
|
|
898
|
+
steps: {
|
|
899
|
+
step: {
|
|
900
|
+
title: 'Step',
|
|
901
|
+
uiMode: (ctx: any) => ({ type: 'drawer', props: { title: 'Function Title', width: 800 } }),
|
|
902
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
const drawer = vi.fn((opts: any) => {
|
|
908
|
+
expect(opts.title).toBe('Function Title');
|
|
909
|
+
expect(opts.width).toBe(800);
|
|
910
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
911
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
912
|
+
return dlg;
|
|
913
|
+
});
|
|
914
|
+
const dialog = vi.fn();
|
|
915
|
+
|
|
916
|
+
model.context.defineProperty('viewer', { value: { drawer, dialog } });
|
|
917
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
918
|
+
|
|
919
|
+
await flowSettings.open({ model, flowKey: 'flowWithFunctionUiMode', stepKey: 'step' } as any);
|
|
920
|
+
|
|
921
|
+
expect(drawer).toHaveBeenCalledTimes(1);
|
|
922
|
+
expect(dialog).not.toHaveBeenCalled();
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('resolves async function-based step uiMode when single step is rendered', async () => {
|
|
926
|
+
const engine = new FlowEngine();
|
|
927
|
+
const flowSettings = new FlowSettings(engine);
|
|
928
|
+
const model = new FlowModel({ uid: 'm-step-uimode-async-function', flowEngine: engine });
|
|
929
|
+
|
|
930
|
+
const M = model.constructor as any;
|
|
931
|
+
M.registerFlow({
|
|
932
|
+
key: 'flowWithAsyncFunctionUiMode',
|
|
933
|
+
steps: {
|
|
934
|
+
step: {
|
|
935
|
+
title: 'Step',
|
|
936
|
+
uiMode: async (ctx: any) => {
|
|
937
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
938
|
+
return { type: 'dialog', props: { title: 'Async Function Title', width: 900 } };
|
|
939
|
+
},
|
|
940
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
const dialog = vi.fn((opts: any) => {
|
|
946
|
+
expect(opts.title).toBe('Async Function Title');
|
|
947
|
+
expect(opts.width).toBe(900);
|
|
948
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
949
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
950
|
+
return dlg;
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
954
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
955
|
+
|
|
956
|
+
await flowSettings.open({ model, flowKey: 'flowWithAsyncFunctionUiMode', stepKey: 'step' } as any);
|
|
957
|
+
|
|
958
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('uses step static uiMode object when single step is rendered', async () => {
|
|
962
|
+
const engine = new FlowEngine();
|
|
963
|
+
const flowSettings = new FlowSettings(engine);
|
|
964
|
+
const model = new FlowModel({ uid: 'm-step-uimode-static', flowEngine: engine });
|
|
965
|
+
|
|
966
|
+
const M = model.constructor as any;
|
|
967
|
+
M.registerFlow({
|
|
968
|
+
key: 'flowWithStaticUiMode',
|
|
969
|
+
steps: {
|
|
970
|
+
step: {
|
|
971
|
+
title: 'Step',
|
|
972
|
+
uiMode: { type: 'drawer', props: { title: 'Static Title', width: 700 } },
|
|
973
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
const drawer = vi.fn((opts: any) => {
|
|
979
|
+
expect(opts.title).toBe('Static Title');
|
|
980
|
+
expect(opts.width).toBe(700);
|
|
981
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
982
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
983
|
+
return dlg;
|
|
984
|
+
});
|
|
985
|
+
const dialog = vi.fn();
|
|
986
|
+
|
|
987
|
+
model.context.defineProperty('viewer', { value: { drawer, dialog } });
|
|
988
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
989
|
+
|
|
990
|
+
await flowSettings.open({ model, flowKey: 'flowWithStaticUiMode', stepKey: 'step' } as any);
|
|
991
|
+
|
|
992
|
+
expect(drawer).toHaveBeenCalledTimes(1);
|
|
993
|
+
expect(dialog).not.toHaveBeenCalled();
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('fallbacks to global uiMode when step has no uiMode and single step is rendered', async () => {
|
|
997
|
+
const engine = new FlowEngine();
|
|
998
|
+
const flowSettings = new FlowSettings(engine);
|
|
999
|
+
const model = new FlowModel({ uid: 'm-step-uimode-fallback', flowEngine: engine });
|
|
1000
|
+
|
|
1001
|
+
const M = model.constructor as any;
|
|
1002
|
+
M.registerFlow({
|
|
1003
|
+
key: 'flowWithoutStepUiMode',
|
|
1004
|
+
steps: {
|
|
1005
|
+
step: {
|
|
1006
|
+
title: 'Step',
|
|
1007
|
+
// no uiMode defined at step level
|
|
1008
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
const drawer = vi.fn((opts: any) => {
|
|
1014
|
+
expect(opts.title).toBe('Global Title');
|
|
1015
|
+
expect(opts.width).toBe(600);
|
|
1016
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1017
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1018
|
+
return dlg;
|
|
1019
|
+
});
|
|
1020
|
+
const dialog = vi.fn();
|
|
1021
|
+
|
|
1022
|
+
model.context.defineProperty('viewer', { value: { drawer, dialog } });
|
|
1023
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1024
|
+
|
|
1025
|
+
await flowSettings.open({
|
|
1026
|
+
model,
|
|
1027
|
+
flowKey: 'flowWithoutStepUiMode',
|
|
1028
|
+
stepKey: 'step',
|
|
1029
|
+
uiMode: { type: 'drawer', props: { title: 'Global Title', width: 600 } },
|
|
1030
|
+
} as any);
|
|
1031
|
+
|
|
1032
|
+
expect(drawer).toHaveBeenCalledTimes(1);
|
|
1033
|
+
expect(dialog).not.toHaveBeenCalled();
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('ignores step uiMode when multiple steps are rendered', async () => {
|
|
1037
|
+
const engine = new FlowEngine();
|
|
1038
|
+
const flowSettings = new FlowSettings(engine);
|
|
1039
|
+
const model = new FlowModel({ uid: 'm-multi-step-ignore-uimode', flowEngine: engine });
|
|
1040
|
+
|
|
1041
|
+
const M = model.constructor as any;
|
|
1042
|
+
M.registerFlow({
|
|
1043
|
+
key: 'flowWithMultiSteps',
|
|
1044
|
+
steps: {
|
|
1045
|
+
step1: {
|
|
1046
|
+
title: 'Step1',
|
|
1047
|
+
uiMode: { type: 'drawer', props: { title: 'Should be ignored', width: 999 } },
|
|
1048
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1049
|
+
},
|
|
1050
|
+
step2: {
|
|
1051
|
+
title: 'Step2',
|
|
1052
|
+
uiMode: (ctx: any) => ({ type: 'drawer', props: { title: 'Should also be ignored', width: 888 } }),
|
|
1053
|
+
uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
const dialog = vi.fn((opts: any) => {
|
|
1059
|
+
// Should use global uiMode setting, not step-level uiMode
|
|
1060
|
+
expect(opts.title).toBe('Global Multi Steps Title');
|
|
1061
|
+
expect(opts.width).toBe(1000);
|
|
1062
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1063
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1064
|
+
return dlg;
|
|
1065
|
+
});
|
|
1066
|
+
const drawer = vi.fn();
|
|
1067
|
+
|
|
1068
|
+
model.context.defineProperty('viewer', { value: { dialog, drawer } });
|
|
1069
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1070
|
+
|
|
1071
|
+
await flowSettings.open({
|
|
1072
|
+
model,
|
|
1073
|
+
flowKey: 'flowWithMultiSteps',
|
|
1074
|
+
uiMode: { type: 'dialog', props: { title: 'Global Multi Steps Title', width: 1000 } },
|
|
1075
|
+
} as any);
|
|
1076
|
+
|
|
1077
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
1078
|
+
expect(drawer).not.toHaveBeenCalled();
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('uses embed uiMode with target element and callbacks', async () => {
|
|
1082
|
+
const engine = new FlowEngine();
|
|
1083
|
+
const flowSettings = new FlowSettings(engine);
|
|
1084
|
+
const model = new FlowModel({ uid: 'm-embed-mode', flowEngine: engine });
|
|
1085
|
+
|
|
1086
|
+
// Create mock DOM element for embed target
|
|
1087
|
+
const mockTarget = document.createElement('div');
|
|
1088
|
+
mockTarget.id = 'nocobase-embed-container';
|
|
1089
|
+
mockTarget.style.width = 'auto';
|
|
1090
|
+
mockTarget.style.maxWidth = 'none';
|
|
1091
|
+
document.body.appendChild(mockTarget);
|
|
1092
|
+
|
|
1093
|
+
// Mock querySelector to return our mock element
|
|
1094
|
+
const originalQuerySelector = document.querySelector;
|
|
1095
|
+
document.querySelector = vi.fn((selector) => {
|
|
1096
|
+
if (selector === '#nocobase-embed-container') {
|
|
1097
|
+
return mockTarget;
|
|
1098
|
+
}
|
|
1099
|
+
return originalQuerySelector.call(document, selector);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
const M = model.constructor as any;
|
|
1103
|
+
M.registerFlow({
|
|
1104
|
+
key: 'embedFlow',
|
|
1105
|
+
steps: {
|
|
1106
|
+
step: {
|
|
1107
|
+
title: 'Step',
|
|
1108
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1109
|
+
},
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const onOpenSpy = vi.fn();
|
|
1114
|
+
const onCloseSpy = vi.fn();
|
|
1115
|
+
const embed = vi.fn((opts: any) => {
|
|
1116
|
+
expect(opts.target).toBe(mockTarget);
|
|
1117
|
+
expect(opts.width).toBe('60%');
|
|
1118
|
+
expect(opts.maxWidth).toBe('900px');
|
|
1119
|
+
expect(typeof opts.onOpen).toBe('function');
|
|
1120
|
+
expect(typeof opts.onClose).toBe('function');
|
|
1121
|
+
|
|
1122
|
+
// Test onOpen callback
|
|
1123
|
+
opts.onOpen();
|
|
1124
|
+
expect(mockTarget.style.width).toBe('60%');
|
|
1125
|
+
expect(mockTarget.style.maxWidth).toBe('900px');
|
|
1126
|
+
expect(onOpenSpy).toHaveBeenCalled();
|
|
1127
|
+
|
|
1128
|
+
// Test onClose callback
|
|
1129
|
+
opts.onClose();
|
|
1130
|
+
expect(mockTarget.style.width).toBe('auto');
|
|
1131
|
+
expect(mockTarget.style.maxWidth).toBe('none');
|
|
1132
|
+
expect(onCloseSpy).toHaveBeenCalled();
|
|
1133
|
+
|
|
1134
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1135
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1136
|
+
return dlg;
|
|
1137
|
+
});
|
|
1138
|
+
const dialog = vi.fn();
|
|
1139
|
+
const drawer = vi.fn();
|
|
1140
|
+
|
|
1141
|
+
model.context.defineProperty('viewer', { value: { embed, dialog, drawer } });
|
|
1142
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1143
|
+
|
|
1144
|
+
await flowSettings.open({
|
|
1145
|
+
model,
|
|
1146
|
+
flowKey: 'embedFlow',
|
|
1147
|
+
stepKey: 'step',
|
|
1148
|
+
uiMode: {
|
|
1149
|
+
type: 'embed',
|
|
1150
|
+
props: {
|
|
1151
|
+
width: '60%',
|
|
1152
|
+
maxWidth: '900px',
|
|
1153
|
+
onOpen: onOpenSpy,
|
|
1154
|
+
onClose: onCloseSpy,
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
} as any);
|
|
1158
|
+
|
|
1159
|
+
expect(embed).toHaveBeenCalledTimes(1);
|
|
1160
|
+
expect(dialog).not.toHaveBeenCalled();
|
|
1161
|
+
expect(drawer).not.toHaveBeenCalled();
|
|
1162
|
+
|
|
1163
|
+
// Cleanup
|
|
1164
|
+
document.body.removeChild(mockTarget);
|
|
1165
|
+
document.querySelector = originalQuerySelector;
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it('uses embed uiMode with default props when target element exists', async () => {
|
|
1169
|
+
const engine = new FlowEngine();
|
|
1170
|
+
const flowSettings = new FlowSettings(engine);
|
|
1171
|
+
const model = new FlowModel({ uid: 'm-embed-default', flowEngine: engine });
|
|
1172
|
+
|
|
1173
|
+
// Create mock DOM element for embed target
|
|
1174
|
+
const mockTarget = document.createElement('div');
|
|
1175
|
+
mockTarget.id = 'nocobase-embed-container';
|
|
1176
|
+
document.body.appendChild(mockTarget);
|
|
1177
|
+
|
|
1178
|
+
// Mock querySelector
|
|
1179
|
+
const originalQuerySelector = document.querySelector;
|
|
1180
|
+
document.querySelector = vi.fn((selector) => {
|
|
1181
|
+
if (selector === '#nocobase-embed-container') {
|
|
1182
|
+
return mockTarget;
|
|
1183
|
+
}
|
|
1184
|
+
return originalQuerySelector.call(document, selector);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
const M = model.constructor as any;
|
|
1188
|
+
M.registerFlow({
|
|
1189
|
+
key: 'embedDefaultFlow',
|
|
1190
|
+
steps: {
|
|
1191
|
+
step: {
|
|
1192
|
+
title: 'Step',
|
|
1193
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1194
|
+
},
|
|
1195
|
+
},
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const embed = vi.fn((opts: any) => {
|
|
1199
|
+
expect(opts.target).toBe(mockTarget);
|
|
1200
|
+
// Test default width and maxWidth
|
|
1201
|
+
opts.onOpen();
|
|
1202
|
+
expect(mockTarget.style.width).toBe('33.3%'); // default width
|
|
1203
|
+
expect(mockTarget.style.maxWidth).toBe('800px'); // default maxWidth
|
|
1204
|
+
|
|
1205
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1206
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1207
|
+
return dlg;
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
model.context.defineProperty('viewer', { value: { embed } });
|
|
1211
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1212
|
+
|
|
1213
|
+
await flowSettings.open({
|
|
1214
|
+
model,
|
|
1215
|
+
flowKey: 'embedDefaultFlow',
|
|
1216
|
+
stepKey: 'step',
|
|
1217
|
+
uiMode: 'embed',
|
|
1218
|
+
} as any);
|
|
1219
|
+
|
|
1220
|
+
expect(embed).toHaveBeenCalledTimes(1);
|
|
1221
|
+
|
|
1222
|
+
// Cleanup
|
|
1223
|
+
document.body.removeChild(mockTarget);
|
|
1224
|
+
document.querySelector = originalQuerySelector;
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('handles embed uiMode when target element is not found', async () => {
|
|
1228
|
+
const engine = new FlowEngine();
|
|
1229
|
+
const flowSettings = new FlowSettings(engine);
|
|
1230
|
+
const model = new FlowModel({ uid: 'm-embed-no-target', flowEngine: engine });
|
|
1231
|
+
|
|
1232
|
+
// Mock querySelector to return null (target not found)
|
|
1233
|
+
const originalQuerySelector = document.querySelector;
|
|
1234
|
+
document.querySelector = vi.fn(() => null);
|
|
1235
|
+
|
|
1236
|
+
const M = model.constructor as any;
|
|
1237
|
+
M.registerFlow({
|
|
1238
|
+
key: 'embedNoTargetFlow',
|
|
1239
|
+
steps: {
|
|
1240
|
+
step: {
|
|
1241
|
+
title: 'Step',
|
|
1242
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
const embed = vi.fn((opts: any) => {
|
|
1248
|
+
expect(opts.target).toBeNull();
|
|
1249
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1250
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1251
|
+
return dlg;
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
model.context.defineProperty('viewer', { value: { embed } });
|
|
1255
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1256
|
+
|
|
1257
|
+
await flowSettings.open({
|
|
1258
|
+
model,
|
|
1259
|
+
flowKey: 'embedNoTargetFlow',
|
|
1260
|
+
stepKey: 'step',
|
|
1261
|
+
uiMode: 'embed',
|
|
1262
|
+
} as any);
|
|
1263
|
+
|
|
1264
|
+
expect(embed).toHaveBeenCalledTimes(1);
|
|
1265
|
+
|
|
1266
|
+
// Restore querySelector
|
|
1267
|
+
document.querySelector = originalQuerySelector;
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
it('handles error in function-based step uiMode gracefully', async () => {
|
|
1271
|
+
const engine = new FlowEngine();
|
|
1272
|
+
const flowSettings = new FlowSettings(engine);
|
|
1273
|
+
const model = new FlowModel({ uid: 'm-step-uimode-error', flowEngine: engine });
|
|
1274
|
+
|
|
1275
|
+
const M = model.constructor as any;
|
|
1276
|
+
M.registerFlow({
|
|
1277
|
+
key: 'flowWithErrorUiMode',
|
|
1278
|
+
steps: {
|
|
1279
|
+
step: {
|
|
1280
|
+
title: 'Step',
|
|
1281
|
+
uiMode: (ctx: any) => {
|
|
1282
|
+
throw new Error('uiMode function error');
|
|
1283
|
+
},
|
|
1284
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// Mock console.error to avoid log noise during test
|
|
1290
|
+
const originalConsoleError = console.error;
|
|
1291
|
+
console.error = vi.fn();
|
|
1292
|
+
|
|
1293
|
+
const dialog = vi.fn((opts: any) => {
|
|
1294
|
+
// Should fallback to default 'dialog' when function throws error
|
|
1295
|
+
expect(typeof opts.title === 'string').toBe(true);
|
|
1296
|
+
const dlg = { close: vi.fn(), Footer: (p: any) => null } as any;
|
|
1297
|
+
if (typeof opts.content === 'function') opts.content(dlg, { defineMethod: vi.fn() });
|
|
1298
|
+
return dlg;
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
1302
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1303
|
+
|
|
1304
|
+
await flowSettings.open({ model, flowKey: 'flowWithErrorUiMode', stepKey: 'step' } as any);
|
|
1305
|
+
|
|
1306
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
1307
|
+
expect(console.error).toHaveBeenCalledWith('Error resolving uiMode function:', expect.any(Error));
|
|
1308
|
+
|
|
1309
|
+
// Restore console.error
|
|
1310
|
+
console.error = originalConsoleError;
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it('supports reactive objects in uiMode function and auto-updates dialog props', async () => {
|
|
1314
|
+
const { observable } = await import('@formily/reactive');
|
|
1315
|
+
|
|
1316
|
+
const engine = new FlowEngine();
|
|
1317
|
+
const flowSettings = new FlowSettings(engine);
|
|
1318
|
+
const model = new FlowModel({ uid: 'm-reactive-uimode', flowEngine: engine });
|
|
1319
|
+
|
|
1320
|
+
// Create reactive state object
|
|
1321
|
+
const reactiveState = observable({
|
|
1322
|
+
title: 'Initial Title',
|
|
1323
|
+
width: 600,
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
const M = model.constructor as any;
|
|
1327
|
+
M.registerFlow({
|
|
1328
|
+
key: 'flowWithReactiveUiMode',
|
|
1329
|
+
steps: {
|
|
1330
|
+
step: {
|
|
1331
|
+
title: 'Step',
|
|
1332
|
+
uiMode: (ctx: any) => ({
|
|
1333
|
+
type: 'dialog',
|
|
1334
|
+
props: {
|
|
1335
|
+
title: reactiveState.title,
|
|
1336
|
+
width: reactiveState.width,
|
|
1337
|
+
},
|
|
1338
|
+
}),
|
|
1339
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
const updateSpy = vi.fn();
|
|
1345
|
+
const closeSpy = vi.fn();
|
|
1346
|
+
const dialog = vi.fn((opts: any) => {
|
|
1347
|
+
// Verify initial props
|
|
1348
|
+
expect(opts.title).toBe('Initial Title');
|
|
1349
|
+
expect(opts.width).toBe(600);
|
|
1350
|
+
|
|
1351
|
+
const dlg = {
|
|
1352
|
+
close: closeSpy,
|
|
1353
|
+
Footer: (p: any) => null,
|
|
1354
|
+
update: updateSpy,
|
|
1355
|
+
} as any;
|
|
1356
|
+
|
|
1357
|
+
if (typeof opts.content === 'function') {
|
|
1358
|
+
opts.content(dlg, { defineMethod: vi.fn() });
|
|
1359
|
+
}
|
|
1360
|
+
return dlg;
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
1364
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1365
|
+
|
|
1366
|
+
await flowSettings.open({ model, flowKey: 'flowWithReactiveUiMode', stepKey: 'step' } as any);
|
|
1367
|
+
|
|
1368
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
1369
|
+
|
|
1370
|
+
// Wait for autorun to be setup
|
|
1371
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1372
|
+
|
|
1373
|
+
// Update reactive state
|
|
1374
|
+
reactiveState.title = 'Updated Title';
|
|
1375
|
+
reactiveState.width = 800;
|
|
1376
|
+
|
|
1377
|
+
// Wait for reactive update
|
|
1378
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1379
|
+
|
|
1380
|
+
// Verify that dialog.update was called with new props
|
|
1381
|
+
expect(updateSpy).toHaveBeenCalledWith({
|
|
1382
|
+
title: 'Updated Title',
|
|
1383
|
+
width: 800,
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it('properly disposes reactive listener when dialog is closed', async () => {
|
|
1388
|
+
const { observable } = await import('@formily/reactive');
|
|
1389
|
+
|
|
1390
|
+
const engine = new FlowEngine();
|
|
1391
|
+
const flowSettings = new FlowSettings(engine);
|
|
1392
|
+
const model = new FlowModel({ uid: 'm-reactive-dispose', flowEngine: engine });
|
|
1393
|
+
|
|
1394
|
+
const reactiveState = observable({
|
|
1395
|
+
title: 'Title',
|
|
1396
|
+
width: 500,
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
const M = model.constructor as any;
|
|
1400
|
+
M.registerFlow({
|
|
1401
|
+
key: 'flowWithReactiveDispose',
|
|
1402
|
+
steps: {
|
|
1403
|
+
step: {
|
|
1404
|
+
title: 'Step',
|
|
1405
|
+
uiMode: (ctx: any) => ({
|
|
1406
|
+
type: 'dialog',
|
|
1407
|
+
props: {
|
|
1408
|
+
title: reactiveState.title,
|
|
1409
|
+
width: reactiveState.width,
|
|
1410
|
+
},
|
|
1411
|
+
}),
|
|
1412
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1413
|
+
},
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
let onCloseFn: Function | undefined;
|
|
1418
|
+
const updateSpy = vi.fn();
|
|
1419
|
+
const dialog = vi.fn((opts: any) => {
|
|
1420
|
+
// Capture the onClose callback
|
|
1421
|
+
onCloseFn = opts.onClose;
|
|
1422
|
+
|
|
1423
|
+
const dlg = {
|
|
1424
|
+
close: vi.fn(),
|
|
1425
|
+
Footer: (p: any) => null,
|
|
1426
|
+
update: updateSpy,
|
|
1427
|
+
} as any;
|
|
1428
|
+
|
|
1429
|
+
if (typeof opts.content === 'function') {
|
|
1430
|
+
opts.content(dlg, { defineMethod: vi.fn() });
|
|
1431
|
+
}
|
|
1432
|
+
return dlg;
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
1436
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1437
|
+
|
|
1438
|
+
await flowSettings.open({ model, flowKey: 'flowWithReactiveDispose', stepKey: 'step' } as any);
|
|
1439
|
+
|
|
1440
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
1441
|
+
expect(typeof onCloseFn).toBe('function');
|
|
1442
|
+
|
|
1443
|
+
// Wait for autorun to be setup
|
|
1444
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1445
|
+
|
|
1446
|
+
// Call the dispose function to simulate dialog close
|
|
1447
|
+
onCloseFn?.();
|
|
1448
|
+
|
|
1449
|
+
// Update reactive state after disposal
|
|
1450
|
+
reactiveState.title = 'Should Not Update';
|
|
1451
|
+
reactiveState.width = 999;
|
|
1452
|
+
|
|
1453
|
+
// Wait to ensure no update occurs
|
|
1454
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1455
|
+
|
|
1456
|
+
// Verify that dialog.update was NOT called after disposal
|
|
1457
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it('handles reactive uiMode when rendering multiple steps (should ignore step-level reactive uiMode)', async () => {
|
|
1461
|
+
const { observable } = await import('@formily/reactive');
|
|
1462
|
+
|
|
1463
|
+
const engine = new FlowEngine();
|
|
1464
|
+
const flowSettings = new FlowSettings(engine);
|
|
1465
|
+
const model = new FlowModel({ uid: 'm-multi-reactive', flowEngine: engine });
|
|
1466
|
+
|
|
1467
|
+
const reactiveState = observable({
|
|
1468
|
+
title: 'Reactive Title',
|
|
1469
|
+
width: 700,
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
const M = model.constructor as any;
|
|
1473
|
+
M.registerFlow({
|
|
1474
|
+
key: 'flowWithMultiStepsReactive',
|
|
1475
|
+
steps: {
|
|
1476
|
+
step1: {
|
|
1477
|
+
title: 'Step1',
|
|
1478
|
+
uiMode: (ctx: any) => ({
|
|
1479
|
+
type: 'dialog',
|
|
1480
|
+
props: {
|
|
1481
|
+
title: reactiveState.title,
|
|
1482
|
+
width: reactiveState.width,
|
|
1483
|
+
},
|
|
1484
|
+
}),
|
|
1485
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1486
|
+
},
|
|
1487
|
+
step2: {
|
|
1488
|
+
title: 'Step2',
|
|
1489
|
+
uiSchema: { g: { type: 'string', 'x-component': 'Input' } },
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
const dialog = vi.fn((opts: any) => {
|
|
1495
|
+
// When multiple steps, should use global uiMode, not step-level reactive uiMode
|
|
1496
|
+
expect(opts.title).toBe('Global Title');
|
|
1497
|
+
expect(opts.width).toBe(1000);
|
|
1498
|
+
|
|
1499
|
+
const dlg = {
|
|
1500
|
+
close: vi.fn(),
|
|
1501
|
+
Footer: (p: any) => null,
|
|
1502
|
+
update: vi.fn(),
|
|
1503
|
+
} as any;
|
|
1504
|
+
|
|
1505
|
+
if (typeof opts.content === 'function') {
|
|
1506
|
+
opts.content(dlg, { defineMethod: vi.fn() });
|
|
1507
|
+
}
|
|
1508
|
+
return dlg;
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
1512
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1513
|
+
|
|
1514
|
+
await flowSettings.open({
|
|
1515
|
+
model,
|
|
1516
|
+
flowKey: 'flowWithMultiStepsReactive',
|
|
1517
|
+
uiMode: { type: 'dialog', props: { title: 'Global Title', width: 1000 } },
|
|
1518
|
+
} as any);
|
|
1519
|
+
|
|
1520
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
it('handles async reactive uiMode function updates', async () => {
|
|
1524
|
+
const { observable } = await import('@formily/reactive');
|
|
1525
|
+
|
|
1526
|
+
const engine = new FlowEngine();
|
|
1527
|
+
const flowSettings = new FlowSettings(engine);
|
|
1528
|
+
const model = new FlowModel({ uid: 'm-async-reactive', flowEngine: engine });
|
|
1529
|
+
|
|
1530
|
+
const reactiveState = observable({
|
|
1531
|
+
title: 'Async Title',
|
|
1532
|
+
width: 650,
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
const M = model.constructor as any;
|
|
1536
|
+
M.registerFlow({
|
|
1537
|
+
key: 'flowWithAsyncReactiveUiMode',
|
|
1538
|
+
steps: {
|
|
1539
|
+
step: {
|
|
1540
|
+
title: 'Step',
|
|
1541
|
+
uiMode: async (ctx: any) => {
|
|
1542
|
+
return {
|
|
1543
|
+
type: 'dialog',
|
|
1544
|
+
props: {
|
|
1545
|
+
title: reactiveState.title,
|
|
1546
|
+
width: reactiveState.width,
|
|
1547
|
+
},
|
|
1548
|
+
};
|
|
1549
|
+
},
|
|
1550
|
+
uiSchema: { f: { type: 'string', 'x-component': 'Input' } },
|
|
1551
|
+
},
|
|
1552
|
+
},
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
const updateSpy = vi.fn();
|
|
1556
|
+
const dialog = vi.fn((opts: any) => {
|
|
1557
|
+
expect(opts.title).toBe('Async Title');
|
|
1558
|
+
expect(opts.width).toBe(650);
|
|
1559
|
+
|
|
1560
|
+
const dlg = {
|
|
1561
|
+
close: vi.fn(),
|
|
1562
|
+
Footer: (p: any) => null,
|
|
1563
|
+
update: updateSpy,
|
|
1564
|
+
} as any;
|
|
1565
|
+
|
|
1566
|
+
if (typeof opts.content === 'function') {
|
|
1567
|
+
opts.content(dlg, { defineMethod: vi.fn() });
|
|
1568
|
+
}
|
|
1569
|
+
return dlg;
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
model.context.defineProperty('viewer', { value: { dialog } });
|
|
1573
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1574
|
+
|
|
1575
|
+
await flowSettings.open({ model, flowKey: 'flowWithAsyncReactiveUiMode', stepKey: 'step' } as any);
|
|
1576
|
+
|
|
1577
|
+
expect(dialog).toHaveBeenCalledTimes(1);
|
|
1578
|
+
|
|
1579
|
+
// Wait for autorun and async uiMode resolution
|
|
1580
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1581
|
+
|
|
1582
|
+
// Update reactive state
|
|
1583
|
+
reactiveState.title = 'Async Updated Title';
|
|
1584
|
+
reactiveState.width = 750;
|
|
1585
|
+
|
|
1586
|
+
// Wait for reactive update
|
|
1587
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1588
|
+
|
|
1589
|
+
// Verify that dialog.update was called with new props
|
|
1590
|
+
expect(updateSpy).toHaveBeenCalledWith({
|
|
1591
|
+
title: 'Async Updated Title',
|
|
1592
|
+
width: 750,
|
|
1593
|
+
});
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// =============================
|
|
1597
|
+
// Tests for currentDialog.submit method assignment
|
|
1598
|
+
// =============================
|
|
1599
|
+
|
|
1600
|
+
it('assigns submit method to currentDialog and can be called externally', async () => {
|
|
1601
|
+
const engine = new FlowEngine();
|
|
1602
|
+
const flowSettings = new FlowSettings(engine);
|
|
1603
|
+
const TestFlowModel = createIsolatedFlowModel('test-submit-1');
|
|
1604
|
+
const model = new TestFlowModel({ uid: 'm-submit-external', flowEngine: engine });
|
|
1605
|
+
|
|
1606
|
+
TestFlowModel.registerFlow({
|
|
1607
|
+
key: 'submitFlow',
|
|
1608
|
+
steps: {
|
|
1609
|
+
step: {
|
|
1610
|
+
title: 'Step',
|
|
1611
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
const info = vi.fn();
|
|
1617
|
+
const error = vi.fn();
|
|
1618
|
+
const success = vi.fn();
|
|
1619
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
1620
|
+
|
|
1621
|
+
const setStepParams = vi.spyOn(model as any, 'setStepParams');
|
|
1622
|
+
const saveStepParams = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
|
|
1623
|
+
|
|
1624
|
+
let capturedDialog: any;
|
|
1625
|
+
model.context.defineProperty('viewer', {
|
|
1626
|
+
value: {
|
|
1627
|
+
dialog: ({ content }) => {
|
|
1628
|
+
capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
|
|
1629
|
+
if (typeof content === 'function') {
|
|
1630
|
+
content(capturedDialog, { defineMethod: vi.fn() });
|
|
1631
|
+
}
|
|
1632
|
+
return capturedDialog;
|
|
1633
|
+
},
|
|
1634
|
+
},
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
await flowSettings.open({ model, flowKey: 'submitFlow', stepKey: 'step' } as any);
|
|
1638
|
+
|
|
1639
|
+
// Verify that submit method was assigned to dialog
|
|
1640
|
+
expect(typeof capturedDialog.submit).toBe('function');
|
|
1641
|
+
|
|
1642
|
+
// Call submit method externally
|
|
1643
|
+
await capturedDialog.submit();
|
|
1644
|
+
|
|
1645
|
+
// Verify that save flow was executed
|
|
1646
|
+
expect(setStepParams).toHaveBeenCalled();
|
|
1647
|
+
expect(saveStepParams).toHaveBeenCalled();
|
|
1648
|
+
expect(success).toHaveBeenCalledWith('Configuration saved');
|
|
1649
|
+
expect(capturedDialog.close).toHaveBeenCalled();
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
it('submit method handles multiple steps correctly', async () => {
|
|
1653
|
+
const engine = new FlowEngine();
|
|
1654
|
+
const flowSettings = new FlowSettings(engine);
|
|
1655
|
+
const TestFlowModel = createIsolatedFlowModel('test-submit-2');
|
|
1656
|
+
const model = new TestFlowModel({ uid: 'm-submit-multi', flowEngine: engine });
|
|
1657
|
+
|
|
1658
|
+
const beforeHook1 = vi.fn();
|
|
1659
|
+
const afterHook1 = vi.fn();
|
|
1660
|
+
const beforeHook2 = vi.fn();
|
|
1661
|
+
const afterHook2 = vi.fn();
|
|
1662
|
+
|
|
1663
|
+
TestFlowModel.registerFlow({
|
|
1664
|
+
key: 'multiStepFlow',
|
|
1665
|
+
steps: {
|
|
1666
|
+
step1: {
|
|
1667
|
+
title: 'Step 1',
|
|
1668
|
+
beforeParamsSave: beforeHook1,
|
|
1669
|
+
afterParamsSave: afterHook1,
|
|
1670
|
+
uiSchema: { field1: { type: 'string', 'x-component': 'Input' } },
|
|
1671
|
+
},
|
|
1672
|
+
step2: {
|
|
1673
|
+
title: 'Step 2',
|
|
1674
|
+
beforeParamsSave: beforeHook2,
|
|
1675
|
+
afterParamsSave: afterHook2,
|
|
1676
|
+
uiSchema: { field2: { type: 'string', 'x-component': 'Input' } },
|
|
1677
|
+
},
|
|
1678
|
+
},
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1682
|
+
|
|
1683
|
+
const setStepParams = vi.spyOn(model as any, 'setStepParams');
|
|
1684
|
+
const saveStepParams = vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
|
|
1685
|
+
|
|
1686
|
+
let capturedDialog: any;
|
|
1687
|
+
model.context.defineProperty('viewer', {
|
|
1688
|
+
value: {
|
|
1689
|
+
dialog: ({ content }) => {
|
|
1690
|
+
capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
|
|
1691
|
+
if (typeof content === 'function') {
|
|
1692
|
+
content(capturedDialog, { defineMethod: vi.fn() });
|
|
1693
|
+
}
|
|
1694
|
+
return capturedDialog;
|
|
1695
|
+
},
|
|
1696
|
+
},
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
await flowSettings.open({ model, flowKey: 'multiStepFlow' } as any);
|
|
1700
|
+
|
|
1701
|
+
// Call submit method
|
|
1702
|
+
await capturedDialog.submit();
|
|
1703
|
+
|
|
1704
|
+
// Verify both steps were processed
|
|
1705
|
+
expect(setStepParams).toHaveBeenCalledTimes(2);
|
|
1706
|
+
expect(setStepParams).toHaveBeenCalledWith('multiStepFlow', 'step1', expect.any(Object));
|
|
1707
|
+
expect(setStepParams).toHaveBeenCalledWith('multiStepFlow', 'step2', expect.any(Object));
|
|
1708
|
+
|
|
1709
|
+
// Verify hooks were called in correct order
|
|
1710
|
+
expect(beforeHook1).toHaveBeenCalled();
|
|
1711
|
+
expect(beforeHook2).toHaveBeenCalled();
|
|
1712
|
+
expect(saveStepParams).toHaveBeenCalled();
|
|
1713
|
+
expect(afterHook1).toHaveBeenCalled();
|
|
1714
|
+
expect(afterHook2).toHaveBeenCalled();
|
|
1715
|
+
|
|
1716
|
+
expect(capturedDialog.close).toHaveBeenCalled();
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
it('submit method handles FlowExitException by closing dialog without error message', async () => {
|
|
1720
|
+
const { FlowExitException } = await import('../utils/exceptions');
|
|
1721
|
+
|
|
1722
|
+
const engine = new FlowEngine();
|
|
1723
|
+
const flowSettings = new FlowSettings(engine);
|
|
1724
|
+
const TestFlowModel = createIsolatedFlowModel('test-submit-3');
|
|
1725
|
+
const model = new TestFlowModel({ uid: 'm-submit-exit', flowEngine: engine });
|
|
1726
|
+
|
|
1727
|
+
TestFlowModel.registerFlow({
|
|
1728
|
+
key: 'exitFlow',
|
|
1729
|
+
steps: {
|
|
1730
|
+
step: {
|
|
1731
|
+
title: 'Step',
|
|
1732
|
+
beforeParamsSave: () => {
|
|
1733
|
+
throw new FlowExitException('exitFlow', 'm-submit-exit', 'Exit requested');
|
|
1734
|
+
},
|
|
1735
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
const info = vi.fn();
|
|
1741
|
+
const error = vi.fn();
|
|
1742
|
+
const success = vi.fn();
|
|
1743
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
1744
|
+
|
|
1745
|
+
vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
|
|
1746
|
+
|
|
1747
|
+
let capturedDialog: any;
|
|
1748
|
+
model.context.defineProperty('viewer', {
|
|
1749
|
+
value: {
|
|
1750
|
+
dialog: ({ content }) => {
|
|
1751
|
+
capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
|
|
1752
|
+
if (typeof content === 'function') {
|
|
1753
|
+
content(capturedDialog, { defineMethod: vi.fn() });
|
|
1754
|
+
}
|
|
1755
|
+
return capturedDialog;
|
|
1756
|
+
},
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
await flowSettings.open({ model, flowKey: 'exitFlow', stepKey: 'step' } as any);
|
|
1761
|
+
|
|
1762
|
+
// Call submit method
|
|
1763
|
+
await capturedDialog.submit();
|
|
1764
|
+
|
|
1765
|
+
// Verify FlowExitException handling
|
|
1766
|
+
expect(error).not.toHaveBeenCalled(); // Should not show error message
|
|
1767
|
+
expect(success).not.toHaveBeenCalled(); // Should not show success message
|
|
1768
|
+
expect(capturedDialog.close).toHaveBeenCalled(); // Should close dialog
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
it('submit method handles general errors by showing error message and keeping dialog open', async () => {
|
|
1772
|
+
const engine = new FlowEngine();
|
|
1773
|
+
const flowSettings = new FlowSettings(engine);
|
|
1774
|
+
const TestFlowModel = createIsolatedFlowModel('test-submit-4');
|
|
1775
|
+
const model = new TestFlowModel({ uid: 'm-submit-error', flowEngine: engine });
|
|
1776
|
+
|
|
1777
|
+
TestFlowModel.registerFlow({
|
|
1778
|
+
key: 'errorFlow',
|
|
1779
|
+
steps: {
|
|
1780
|
+
step: {
|
|
1781
|
+
title: 'Step',
|
|
1782
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
1783
|
+
},
|
|
1784
|
+
},
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
const info = vi.fn();
|
|
1788
|
+
const error = vi.fn();
|
|
1789
|
+
const success = vi.fn();
|
|
1790
|
+
model.context.defineProperty('message', { value: { info, error, success } });
|
|
1791
|
+
|
|
1792
|
+
// Mock saveStepParams to throw error
|
|
1793
|
+
vi.spyOn(model as any, 'saveStepParams').mockRejectedValue(new Error('Save failed'));
|
|
1794
|
+
|
|
1795
|
+
let capturedDialog: any;
|
|
1796
|
+
model.context.defineProperty('viewer', {
|
|
1797
|
+
value: {
|
|
1798
|
+
dialog: ({ content }) => {
|
|
1799
|
+
capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
|
|
1800
|
+
if (typeof content === 'function') {
|
|
1801
|
+
content(capturedDialog, { defineMethod: vi.fn() });
|
|
1802
|
+
}
|
|
1803
|
+
return capturedDialog;
|
|
1804
|
+
},
|
|
1805
|
+
},
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
// Mock console.error to avoid log noise
|
|
1809
|
+
const originalConsoleError = console.error;
|
|
1810
|
+
console.error = vi.fn();
|
|
1811
|
+
|
|
1812
|
+
await flowSettings.open({ model, flowKey: 'errorFlow', stepKey: 'step' } as any);
|
|
1813
|
+
|
|
1814
|
+
// Call submit method
|
|
1815
|
+
await capturedDialog.submit();
|
|
1816
|
+
|
|
1817
|
+
// Verify error handling
|
|
1818
|
+
expect(error).toHaveBeenCalledWith('Error saving configuration, please check console');
|
|
1819
|
+
expect(success).not.toHaveBeenCalled();
|
|
1820
|
+
expect(capturedDialog.close).not.toHaveBeenCalled(); // Should keep dialog open
|
|
1821
|
+
expect(console.error).toHaveBeenCalledWith('FlowSettings.open: save error', expect.any(Error));
|
|
1822
|
+
|
|
1823
|
+
// Restore console.error
|
|
1824
|
+
console.error = originalConsoleError;
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
it('submit method calls onSaved callback after successful save', async () => {
|
|
1828
|
+
const engine = new FlowEngine();
|
|
1829
|
+
const flowSettings = new FlowSettings(engine);
|
|
1830
|
+
const TestFlowModel = createIsolatedFlowModel('test-submit-5');
|
|
1831
|
+
const model = new TestFlowModel({ uid: 'm-submit-callback', flowEngine: engine });
|
|
1832
|
+
|
|
1833
|
+
TestFlowModel.registerFlow({
|
|
1834
|
+
key: 'callbackFlow',
|
|
1835
|
+
steps: {
|
|
1836
|
+
step: {
|
|
1837
|
+
title: 'Step',
|
|
1838
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1844
|
+
vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
|
|
1845
|
+
|
|
1846
|
+
let capturedDialog: any;
|
|
1847
|
+
model.context.defineProperty('viewer', {
|
|
1848
|
+
value: {
|
|
1849
|
+
dialog: ({ content }) => {
|
|
1850
|
+
capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
|
|
1851
|
+
if (typeof content === 'function') {
|
|
1852
|
+
content(capturedDialog, { defineMethod: vi.fn() });
|
|
1853
|
+
}
|
|
1854
|
+
return capturedDialog;
|
|
1855
|
+
},
|
|
1856
|
+
},
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
const onSaved = vi.fn();
|
|
1860
|
+
await flowSettings.open({ model, flowKey: 'callbackFlow', stepKey: 'step', onSaved } as any);
|
|
1861
|
+
|
|
1862
|
+
// Call submit method
|
|
1863
|
+
await capturedDialog.submit();
|
|
1864
|
+
|
|
1865
|
+
// Verify onSaved callback was called
|
|
1866
|
+
expect(onSaved).toHaveBeenCalledTimes(1);
|
|
1867
|
+
expect(capturedDialog.close).toHaveBeenCalled();
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
it('submit method handles onSaved callback errors gracefully', async () => {
|
|
1871
|
+
const engine = new FlowEngine();
|
|
1872
|
+
const flowSettings = new FlowSettings(engine);
|
|
1873
|
+
const TestFlowModel = createIsolatedFlowModel('test-submit-6');
|
|
1874
|
+
const model = new TestFlowModel({ uid: 'm-submit-callback-error', flowEngine: engine });
|
|
1875
|
+
|
|
1876
|
+
TestFlowModel.registerFlow({
|
|
1877
|
+
key: 'callbackErrorFlow',
|
|
1878
|
+
steps: {
|
|
1879
|
+
step: {
|
|
1880
|
+
title: 'Step',
|
|
1881
|
+
uiSchema: { field: { type: 'string', 'x-component': 'Input' } },
|
|
1882
|
+
},
|
|
1883
|
+
},
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
model.context.defineProperty('message', { value: { info: vi.fn(), error: vi.fn(), success: vi.fn() } });
|
|
1887
|
+
vi.spyOn(model as any, 'saveStepParams').mockResolvedValue(undefined);
|
|
1888
|
+
|
|
1889
|
+
let capturedDialog: any;
|
|
1890
|
+
model.context.defineProperty('viewer', {
|
|
1891
|
+
value: {
|
|
1892
|
+
dialog: ({ content }) => {
|
|
1893
|
+
capturedDialog = { close: vi.fn(), Footer: (p: any) => null };
|
|
1894
|
+
if (typeof content === 'function') {
|
|
1895
|
+
content(capturedDialog, { defineMethod: vi.fn() });
|
|
1896
|
+
}
|
|
1897
|
+
return capturedDialog;
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
// Mock console.error to avoid log noise
|
|
1903
|
+
const originalConsoleError = console.error;
|
|
1904
|
+
console.error = vi.fn();
|
|
1905
|
+
|
|
1906
|
+
const onSaved = vi.fn().mockRejectedValue(new Error('Callback error'));
|
|
1907
|
+
await flowSettings.open({ model, flowKey: 'callbackErrorFlow', stepKey: 'step', onSaved } as any);
|
|
1908
|
+
|
|
1909
|
+
// Call submit method
|
|
1910
|
+
await capturedDialog.submit();
|
|
1911
|
+
|
|
1912
|
+
// Verify that main save process completed successfully despite callback error
|
|
1913
|
+
expect(onSaved).toHaveBeenCalledTimes(1);
|
|
1914
|
+
expect(capturedDialog.close).toHaveBeenCalled();
|
|
1915
|
+
expect(console.error).toHaveBeenCalledWith('FlowSettings.open: onSaved callback error', expect.any(Error));
|
|
1916
|
+
|
|
1917
|
+
// Restore console.error
|
|
1918
|
+
console.error = originalConsoleError;
|
|
1919
|
+
});
|
|
1920
|
+
});
|