@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +2 -0
- package/lib/JSRunner.d.ts +15 -0
- package/lib/JSRunner.js +82 -7
- package/lib/ViewScopedFlowEngine.js +8 -1
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +59 -3
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +21 -3
- package/lib/components/subModel/AddSubModelButton.js +16 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +84 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +190 -26
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2270 -148
- package/lib/flowEngine.d.ts +160 -1
- package/lib/flowEngine.js +383 -26
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +51 -17
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.d.ts +7 -0
- package/lib/models/flowModel.js +83 -8
- package/lib/provider.js +7 -6
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +22 -9
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +7 -1
- package/lib/scheduler/ModelOperationScheduler.js +28 -23
- package/lib/types.d.ts +63 -1
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +49 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +17 -1
- package/lib/utils/schema-utils.js +80 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +28 -6
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +27 -5
- package/lib/views/usePage.d.ts +6 -1
- package/lib/views/usePage.js +53 -9
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +5 -5
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +111 -5
- package/src/ViewScopedFlowEngine.ts +8 -0
- package/src/__tests__/JSRunner.test.ts +91 -1
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +23 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +3 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +72 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
- package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
- package/src/components/dnd/gridDragPlanner.ts +68 -2
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +31 -4
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/subModel/AddSubModelButton.tsx +17 -1
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
- package/src/components/subModel/utils.ts +1 -1
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +88 -110
- package/src/executor/FlowExecutor.ts +230 -28
- package/src/executor/__tests__/flowExecutor.test.ts +123 -0
- package/src/flowContext.ts +2989 -212
- package/src/flowEngine.ts +427 -22
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +58 -18
- package/src/index.ts +14 -1
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +768 -0
- package/src/models/__tests__/flowModel.clone.test.ts +416 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +112 -7
- package/src/provider.tsx +9 -7
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +24 -9
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +41 -24
- package/src/types.ts +86 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +157 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +38 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +109 -1
- package/src/views/FlowView.tsx +11 -1
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +34 -5
- package/src/views/useDrawer.tsx +33 -4
- package/src/views/usePage.tsx +63 -8
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -13,7 +13,7 @@ import { render, act, waitFor, screen } from '@testing-library/react';
|
|
|
13
13
|
import { FlowEngine } from '../../flowEngine';
|
|
14
14
|
import { FlowEngineProvider } from '../../provider';
|
|
15
15
|
import { FlowViewer } from '../FlowView';
|
|
16
|
-
import { usePage } from '../usePage';
|
|
16
|
+
import { usePage, GLOBAL_EMBED_CONTAINER_ID } from '../usePage';
|
|
17
17
|
import { App, ConfigProvider } from 'antd';
|
|
18
18
|
|
|
19
19
|
describe('FlowViewer zIndex with usePage', () => {
|
|
@@ -130,4 +130,57 @@ describe('FlowViewer zIndex with usePage', () => {
|
|
|
130
130
|
|
|
131
131
|
unmount();
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
it('replaces previous embed view when using global #nocobase-embed-container target', async () => {
|
|
135
|
+
let api: { open: (config: any, flowContext: any) => any } | undefined;
|
|
136
|
+
|
|
137
|
+
function TestApp({ onReady }: { onReady: (page: any) => void }) {
|
|
138
|
+
const [page, pageHolder] = usePage() as [{ open: (config: any, flowContext: any) => any }, React.ReactNode];
|
|
139
|
+
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
onReady(page);
|
|
142
|
+
}, [page, onReady]);
|
|
143
|
+
|
|
144
|
+
return <>{pageHolder}</>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const Wrapper: React.FC<{ onReady: (page: any) => void }> = ({ onReady }) => (
|
|
148
|
+
<ConfigProvider>
|
|
149
|
+
<App>
|
|
150
|
+
<FlowEngineProvider engine={engine}>
|
|
151
|
+
<TestApp onReady={onReady} />
|
|
152
|
+
</FlowEngineProvider>
|
|
153
|
+
</App>
|
|
154
|
+
</ConfigProvider>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const target = document.createElement('div');
|
|
158
|
+
target.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
159
|
+
document.body.appendChild(target);
|
|
160
|
+
|
|
161
|
+
const { unmount } = render(
|
|
162
|
+
<Wrapper
|
|
163
|
+
onReady={(page) => {
|
|
164
|
+
api = page;
|
|
165
|
+
}}
|
|
166
|
+
/>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await waitFor(() => expect(api).toBeDefined());
|
|
170
|
+
|
|
171
|
+
await act(async () => {
|
|
172
|
+
api!.open({ target, content: <div data-testid="page1">Page 1</div> }, engine.context);
|
|
173
|
+
});
|
|
174
|
+
await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
|
|
175
|
+
|
|
176
|
+
// Opening page2 into the global embed container should destroy page1 (replace behavior).
|
|
177
|
+
await act(async () => {
|
|
178
|
+
api!.open({ target, content: <div data-testid="page2">Page 2</div> }, engine.context);
|
|
179
|
+
});
|
|
180
|
+
await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument());
|
|
181
|
+
expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
|
|
182
|
+
|
|
183
|
+
unmount();
|
|
184
|
+
document.body.removeChild(target);
|
|
185
|
+
});
|
|
133
186
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { runViewBeforeClose } from '../runViewBeforeClose';
|
|
12
|
+
|
|
13
|
+
describe('runViewBeforeClose', () => {
|
|
14
|
+
it('returns true when no beforeClose handler is configured', async () => {
|
|
15
|
+
await expect(runViewBeforeClose({} as any, { force: false })).resolves.toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('skips beforeClose handler for force close', async () => {
|
|
19
|
+
const beforeClose = vi.fn();
|
|
20
|
+
|
|
21
|
+
await expect(runViewBeforeClose({ beforeClose } as any, { force: true })).resolves.toBe(true);
|
|
22
|
+
expect(beforeClose).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns false when beforeClose handler blocks the close', async () => {
|
|
26
|
+
const beforeClose = vi.fn().mockResolvedValue(false);
|
|
27
|
+
|
|
28
|
+
await expect(runViewBeforeClose({ beforeClose } as any, { force: false })).resolves.toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -14,30 +14,29 @@ import { useDialog } from '../useDialog';
|
|
|
14
14
|
import { FlowContext } from '../../flowContext';
|
|
15
15
|
|
|
16
16
|
// Mock dependencies
|
|
17
|
-
vi.mock('
|
|
17
|
+
vi.mock('../../provider', () => ({
|
|
18
18
|
FlowEngineProvider: ({ children }) => children,
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
-
vi.mock('
|
|
21
|
+
vi.mock('../../FlowContextProvider', () => ({
|
|
22
22
|
FlowViewContextProvider: ({ children }) => children,
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
-
vi.mock('
|
|
25
|
+
vi.mock('../../ViewScopedFlowEngine', () => ({
|
|
26
26
|
createViewScopedEngine: (engine) => ({
|
|
27
27
|
context: new FlowContext(),
|
|
28
28
|
unlinkFromStack: vi.fn(),
|
|
29
|
+
setDestroyView: vi.fn(),
|
|
30
|
+
// mimic real view stack linkage: previousEngine points to the last engine in chain
|
|
31
|
+
previousEngine: (engine as any)?.nextEngine || engine,
|
|
29
32
|
}),
|
|
30
33
|
}));
|
|
31
34
|
|
|
32
|
-
vi.mock('
|
|
35
|
+
vi.mock('../../utils/variablesParams', () => ({
|
|
33
36
|
createViewRecordResolveOnServer: vi.fn(),
|
|
34
37
|
getViewRecordFromParent: vi.fn(),
|
|
35
38
|
}));
|
|
36
39
|
|
|
37
|
-
vi.mock('../createViewMeta', () => ({
|
|
38
|
-
registerPopupVariable: vi.fn(),
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
40
|
vi.mock('../DialogComponent', () => ({
|
|
42
41
|
default: ({ children }) => <div>{children}</div>,
|
|
43
42
|
}));
|
|
@@ -52,8 +51,12 @@ vi.mock('../usePatchElement', () => ({
|
|
|
52
51
|
describe('useDialog - close/destroy logic', () => {
|
|
53
52
|
const createMockFlowContext = () => {
|
|
54
53
|
const ctx = new FlowContext();
|
|
54
|
+
ctx.defineMethod('t', (key: string) => key);
|
|
55
55
|
ctx.engine = {
|
|
56
56
|
context: new FlowContext(),
|
|
57
|
+
emitter: {
|
|
58
|
+
emit: vi.fn(),
|
|
59
|
+
},
|
|
57
60
|
};
|
|
58
61
|
return ctx;
|
|
59
62
|
};
|
|
@@ -73,40 +76,40 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
73
76
|
return api;
|
|
74
77
|
};
|
|
75
78
|
|
|
76
|
-
it('should call destroy (and thus closeFunc) when close is called without preventClose', () => {
|
|
79
|
+
it('should call destroy (and thus closeFunc) when close is called without preventClose', async () => {
|
|
77
80
|
const api = renderUseDialog();
|
|
78
81
|
const flowContext = createMockFlowContext();
|
|
79
82
|
|
|
80
83
|
const dialog = api.open({}, flowContext);
|
|
81
84
|
|
|
82
|
-
dialog.close();
|
|
85
|
+
await dialog.close();
|
|
83
86
|
|
|
84
87
|
expect(mockCloseFunc).toHaveBeenCalled();
|
|
85
88
|
});
|
|
86
89
|
|
|
87
|
-
it('should not call destroy (and thus closeFunc) when close is called with preventClose=true', () => {
|
|
90
|
+
it('should not call destroy (and thus closeFunc) when close is called with preventClose=true', async () => {
|
|
88
91
|
const api = renderUseDialog();
|
|
89
92
|
const flowContext = createMockFlowContext();
|
|
90
93
|
|
|
91
94
|
const dialog = api.open({ preventClose: true }, flowContext);
|
|
92
95
|
|
|
93
|
-
dialog.close();
|
|
96
|
+
await dialog.close();
|
|
94
97
|
|
|
95
98
|
expect(mockCloseFunc).not.toHaveBeenCalled();
|
|
96
99
|
});
|
|
97
100
|
|
|
98
|
-
it('should call destroy (and thus closeFunc) when close is called with preventClose=true but force=true', () => {
|
|
101
|
+
it('should call destroy (and thus closeFunc) when close is called with preventClose=true but force=true', async () => {
|
|
99
102
|
const api = renderUseDialog();
|
|
100
103
|
const flowContext = createMockFlowContext();
|
|
101
104
|
|
|
102
105
|
const dialog = api.open({ preventClose: true }, flowContext);
|
|
103
106
|
|
|
104
|
-
dialog.close(undefined, true);
|
|
107
|
+
await dialog.close(undefined, true);
|
|
105
108
|
|
|
106
109
|
expect(mockCloseFunc).toHaveBeenCalled();
|
|
107
110
|
});
|
|
108
111
|
|
|
109
|
-
it('should delegate to navigation.back when triggerByRouter is true', () => {
|
|
112
|
+
it('should delegate to navigation.back when triggerByRouter is true', async () => {
|
|
110
113
|
const api = renderUseDialog();
|
|
111
114
|
const flowContext = createMockFlowContext();
|
|
112
115
|
const backMock = vi.fn();
|
|
@@ -123,10 +126,35 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
123
126
|
flowContext,
|
|
124
127
|
);
|
|
125
128
|
|
|
126
|
-
dialog.close();
|
|
129
|
+
await dialog.close();
|
|
127
130
|
|
|
128
131
|
expect(backMock).toHaveBeenCalled();
|
|
129
132
|
// Should not call destroy directly, let router handle it
|
|
130
133
|
expect(mockCloseFunc).not.toHaveBeenCalled();
|
|
131
134
|
});
|
|
135
|
+
|
|
136
|
+
it('should emit view activated event on opener engine', async () => {
|
|
137
|
+
const api = renderUseDialog();
|
|
138
|
+
const flowContext = createMockFlowContext();
|
|
139
|
+
const emitSpy = flowContext.engine.emitter.emit;
|
|
140
|
+
|
|
141
|
+
const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
|
|
142
|
+
|
|
143
|
+
await dialog.close();
|
|
144
|
+
expect(emitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should emit view events on immediate opener engine (previousEngine) when present', async () => {
|
|
148
|
+
const api = renderUseDialog();
|
|
149
|
+
const flowContext = createMockFlowContext();
|
|
150
|
+
const rootEmitSpy = flowContext.engine.emitter.emit;
|
|
151
|
+
const openerEmitSpy = vi.fn();
|
|
152
|
+
(flowContext.engine as any).nextEngine = { emitter: { emit: openerEmitSpy }, __NOCOBASE_ENGINE_SCOPE__: 'view' };
|
|
153
|
+
|
|
154
|
+
const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
|
|
155
|
+
|
|
156
|
+
await dialog.close();
|
|
157
|
+
expect(openerEmitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
|
|
158
|
+
expect(rootEmitSpy).not.toHaveBeenCalledWith('view:activated', expect.anything());
|
|
159
|
+
});
|
|
132
160
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../../flowEngine';
|
|
12
|
+
import { createViewScopedEngine } from '../../ViewScopedFlowEngine';
|
|
13
|
+
import { resolveOpenerEngine } from '../viewEvents';
|
|
14
|
+
|
|
15
|
+
describe('viewEvents.resolveOpenerEngine', () => {
|
|
16
|
+
it('prefers the parent view engine even when it is not the stack tail (cached page scenario)', () => {
|
|
17
|
+
const root = new FlowEngine();
|
|
18
|
+
const pageA = createViewScopedEngine(root);
|
|
19
|
+
createViewScopedEngine(root); // pageB appended after pageA
|
|
20
|
+
|
|
21
|
+
// Open a dialog from pageA while another kept-alive view exists after it.
|
|
22
|
+
// view scoped engines always link to the tail, so the dialog's previousEngine will be pageB.
|
|
23
|
+
const dialog = createViewScopedEngine(pageA);
|
|
24
|
+
|
|
25
|
+
const opener = resolveOpenerEngine(pageA, dialog);
|
|
26
|
+
expect(opener).toBe(pageA);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -70,66 +70,6 @@ function makeMetaFromValue(value: any, title?: string, seen?: WeakSet<any>): any
|
|
|
70
70
|
return { type: 'any', title };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
74
|
-
* Create a meta factory for ctx.view that includes:
|
|
75
|
-
* - buildVariablesParams: { record } via inferRecordRef
|
|
76
|
-
* - properties.record: full collection meta via buildRecordMeta
|
|
77
|
-
* - type/preventClose/inputArgs/navigation fields for better variable selection UX
|
|
78
|
-
*/
|
|
79
|
-
export function createViewMeta(ctx: FlowContext): PropertyMetaFactory {
|
|
80
|
-
const viewTitle = ctx.t('当前视图');
|
|
81
|
-
const factory: PropertyMetaFactory = async () => {
|
|
82
|
-
const view = ctx.view;
|
|
83
|
-
return {
|
|
84
|
-
type: 'object',
|
|
85
|
-
title: ctx.t('当前视图'),
|
|
86
|
-
buildVariablesParams: (c) => {
|
|
87
|
-
const params = inferViewRecordRef(c);
|
|
88
|
-
if (params) {
|
|
89
|
-
return {
|
|
90
|
-
record: params,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
return undefined;
|
|
94
|
-
},
|
|
95
|
-
properties: async () => {
|
|
96
|
-
const props: Record<string, any> = {};
|
|
97
|
-
// 仅当能推断到当前记录引用时,才暴露“当前视图记录”,避免出现空子菜单
|
|
98
|
-
const refNow = inferViewRecordRef(ctx);
|
|
99
|
-
if (refNow && refNow.collection) {
|
|
100
|
-
const recordFactory: PropertyMetaFactory = async () => {
|
|
101
|
-
try {
|
|
102
|
-
const ref = inferViewRecordRef(ctx);
|
|
103
|
-
if (!ref?.collection) return null;
|
|
104
|
-
const dsKey = ref.dataSourceKey || 'main';
|
|
105
|
-
const ds = ctx.dataSourceManager?.getDataSource?.(dsKey);
|
|
106
|
-
const col = ds?.collectionManager?.getCollection?.(ref.collection);
|
|
107
|
-
if (!col) return null;
|
|
108
|
-
return (await buildRecordMeta(
|
|
109
|
-
() => col,
|
|
110
|
-
ctx.t('当前视图记录'),
|
|
111
|
-
(c) => inferViewRecordRef(c),
|
|
112
|
-
)) as PropertyMeta;
|
|
113
|
-
} catch (e) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
recordFactory.title = ctx.t('当前视图记录');
|
|
118
|
-
recordFactory.hasChildren = true;
|
|
119
|
-
props.record = recordFactory;
|
|
120
|
-
}
|
|
121
|
-
props.type = { type: 'string', title: ctx.t?.('类型') || '类型' };
|
|
122
|
-
props.preventClose = { type: 'boolean', title: ctx.t?.('是否允许关闭') || '是否允许关闭' };
|
|
123
|
-
props.inputArgs = makeMetaFromValue(view?.inputArgs, ctx.t?.('输入参数') || '输入参数');
|
|
124
|
-
return props;
|
|
125
|
-
},
|
|
126
|
-
} as PropertyMeta;
|
|
127
|
-
};
|
|
128
|
-
// 设置工厂函数的 title,让未加载前的占位标题就是“当前视图”
|
|
129
|
-
factory.title = viewTitle;
|
|
130
|
-
return factory;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
73
|
/**
|
|
134
74
|
* 为 ctx.popup 构建元信息:
|
|
135
75
|
* - popup.record:当前弹窗记录(服务端解析)
|
|
@@ -142,14 +82,16 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
|
|
|
142
82
|
const isPopupView = (view?: FlowView): boolean => {
|
|
143
83
|
if (!view) return false;
|
|
144
84
|
const stack = Array.isArray(view.navigation?.viewStack) ? view.navigation.viewStack : [];
|
|
145
|
-
|
|
85
|
+
const openerUids = view?.inputArgs?.openerUids;
|
|
86
|
+
const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
|
|
87
|
+
return stack.length >= 2 || hasOpener;
|
|
146
88
|
};
|
|
147
89
|
|
|
148
90
|
const hasPopupNow = (): boolean => isPopupView(anchorView ?? ctx.view);
|
|
149
91
|
|
|
150
92
|
// 统一解析锚定视图下的 RecordRef,避免在设置弹窗等二级视图中被误导
|
|
151
93
|
const resolveRecordRef = async (flowCtx: FlowContext): Promise<RecordRef | undefined> => {
|
|
152
|
-
const view = anchorView ??
|
|
94
|
+
const view = anchorView ?? flowCtx.view;
|
|
153
95
|
if (!view || !isPopupView(view)) return undefined;
|
|
154
96
|
|
|
155
97
|
const base = await buildPopupRuntime(flowCtx, view);
|
|
@@ -353,19 +295,24 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
|
|
|
353
295
|
const props: Record<string, any> = {};
|
|
354
296
|
// 当前弹窗 UID(纯前端变量)
|
|
355
297
|
props.uid = { type: 'string', title: t('Popup uid') };
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
298
|
+
// 仅当存在 filterByTk(可推断具体记录)时才提供“当前弹窗记录”变量;
|
|
299
|
+
// 对于新增/选择类弹窗(无 filterByTk),不应展示该变量以避免误导。
|
|
300
|
+
const recordRef = await resolveRecordRef(ctx);
|
|
301
|
+
if (recordRef) {
|
|
302
|
+
// 基于锚定视图计算“当前弹窗记录”的集合与 RecordRef
|
|
303
|
+
const recordFactory: PropertyMetaFactory = async () => {
|
|
304
|
+
const col = await getCurrentCollection();
|
|
305
|
+
if (!col) return null;
|
|
306
|
+
return await buildRecordMeta(
|
|
307
|
+
() => col,
|
|
308
|
+
t('Current popup record'),
|
|
309
|
+
(c) => resolveRecordRef(c),
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
recordFactory.title = t('Current popup record');
|
|
313
|
+
recordFactory.hasChildren = true;
|
|
314
|
+
props.record = recordFactory;
|
|
315
|
+
}
|
|
369
316
|
// 当 view.inputArgs 带有 sourceId + associationName 时,提供“上级记录”变量(基于 sourceId 推断)
|
|
370
317
|
try {
|
|
371
318
|
const inputArgs = ctx.view?.inputArgs;
|
package/src/views/index.tsx
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
export { useDialog } from './useDialog';
|
|
11
11
|
export { useDrawer } from './useDrawer';
|
|
12
|
-
export { usePage } from './usePage';
|
|
12
|
+
export { usePage, GLOBAL_EMBED_CONTAINER_ID, EMBED_REPLACING_DATA_KEY } from './usePage';
|
|
13
13
|
export { usePopover } from './usePopover';
|
|
14
14
|
export { ViewNavigation } from './ViewNavigation';
|
|
15
|
-
export { createViewMeta } from './createViewMeta';
|
|
@@ -0,0 +1,19 @@
|
|
|
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 type { FlowView, FlowViewBeforeClosePayload } from './FlowView';
|
|
11
|
+
|
|
12
|
+
export async function runViewBeforeClose(view: FlowView, payload: FlowViewBeforeClosePayload): Promise<boolean> {
|
|
13
|
+
if (payload.force) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = await view.beforeClose?.(payload);
|
|
18
|
+
return result !== false;
|
|
19
|
+
}
|
package/src/views/useDialog.tsx
CHANGED
|
@@ -15,9 +15,11 @@ import { FlowViewContextProvider } from '../FlowContextProvider';
|
|
|
15
15
|
import { registerPopupVariable } from './createViewMeta';
|
|
16
16
|
import DialogComponent from './DialogComponent';
|
|
17
17
|
import usePatchElement from './usePatchElement';
|
|
18
|
+
import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } from './viewEvents';
|
|
18
19
|
import { FlowEngineProvider } from '../provider';
|
|
19
20
|
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
20
21
|
import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
|
|
22
|
+
import { runViewBeforeClose } from './runViewBeforeClose';
|
|
21
23
|
|
|
22
24
|
let uuid = 0;
|
|
23
25
|
|
|
@@ -25,6 +27,7 @@ export function useDialog() {
|
|
|
25
27
|
const holderRef = React.useRef(null);
|
|
26
28
|
|
|
27
29
|
const open = (config, flowContext) => {
|
|
30
|
+
const parentEngine = flowContext?.engine;
|
|
28
31
|
uuid += 1;
|
|
29
32
|
const dialogRef = React.createRef<{
|
|
30
33
|
destroy: () => void;
|
|
@@ -77,6 +80,8 @@ export function useDialog() {
|
|
|
77
80
|
const ctx = new FlowContext();
|
|
78
81
|
// 为当前视图创建作用域引擎(隔离实例与缓存)
|
|
79
82
|
const scopedEngine = createViewScopedEngine(flowContext.engine);
|
|
83
|
+
const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
|
|
84
|
+
|
|
80
85
|
ctx.defineProperty('engine', { value: scopedEngine });
|
|
81
86
|
ctx.addDelegate(scopedEngine.context);
|
|
82
87
|
if (config.inheritContext !== false) {
|
|
@@ -85,32 +90,48 @@ export function useDialog() {
|
|
|
85
90
|
ctx.addDelegate(flowContext.engine.context);
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
// 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
|
|
94
|
+
let destroyed = false;
|
|
95
|
+
|
|
88
96
|
// 构造 currentDialog 实例
|
|
89
97
|
const currentDialog = {
|
|
90
98
|
type: 'dialog' as const,
|
|
91
99
|
inputArgs: config.inputArgs || {},
|
|
92
100
|
preventClose: !!config.preventClose,
|
|
101
|
+
beforeClose: undefined,
|
|
93
102
|
destroy: (result?: any) => {
|
|
103
|
+
if (destroyed) return;
|
|
104
|
+
destroyed = true;
|
|
94
105
|
config.onClose?.();
|
|
95
106
|
dialogRef.current?.destroy();
|
|
96
107
|
closeFunc?.();
|
|
97
108
|
resolvePromise?.(result);
|
|
109
|
+
// Notify opener view that it becomes active again.
|
|
110
|
+
const openerEmitter = openerEngine?.emitter;
|
|
111
|
+
bumpViewActivatedVersion(openerEmitter);
|
|
112
|
+
openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'dialog', viewUid: currentDialog?.inputArgs?.viewUid });
|
|
98
113
|
// 关闭时修正 previous/next 指针
|
|
99
114
|
scopedEngine.unlinkFromStack();
|
|
100
115
|
},
|
|
101
116
|
update: (newConfig) => dialogRef.current?.update(newConfig),
|
|
102
|
-
close: (result?: any, force?: boolean) => {
|
|
117
|
+
close: async (result?: any, force?: boolean) => {
|
|
103
118
|
if (config.preventClose && !force) {
|
|
104
|
-
return;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const shouldClose = await runViewBeforeClose(currentDialog, { result, force });
|
|
123
|
+
if (!shouldClose) {
|
|
124
|
+
return false;
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
|
|
108
128
|
// 交由路由系统来销毁当前视图
|
|
109
129
|
config.inputArgs.navigation.back();
|
|
110
|
-
return;
|
|
130
|
+
return true;
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
currentDialog.destroy(result);
|
|
134
|
+
return true;
|
|
114
135
|
},
|
|
115
136
|
Footer: FooterComponent,
|
|
116
137
|
Header: HeaderComponent,
|
|
@@ -130,9 +151,17 @@ export function useDialog() {
|
|
|
130
151
|
|
|
131
152
|
ctx.defineProperty('view', {
|
|
132
153
|
get: () => currentDialog,
|
|
133
|
-
// meta: createViewMeta(ctx),
|
|
134
154
|
resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
|
|
135
155
|
});
|
|
156
|
+
// 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
|
|
157
|
+
// 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
|
|
158
|
+
// 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
|
|
159
|
+
scopedEngine.setDestroyView(() => {
|
|
160
|
+
if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
|
|
161
|
+
config.inputArgs.navigation.back();
|
|
162
|
+
}
|
|
163
|
+
currentDialog.destroy();
|
|
164
|
+
});
|
|
136
165
|
// 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
|
|
137
166
|
registerPopupVariable(ctx, currentDialog);
|
|
138
167
|
// 内部组件,在 Provider 内部计算 content
|
|
@@ -164,9 +193,9 @@ export function useDialog() {
|
|
|
164
193
|
className="nb-dialog-overflow-hidden"
|
|
165
194
|
ref={dialogRef}
|
|
166
195
|
hidden={config.inputArgs?.hidden?.value}
|
|
167
|
-
{...config}
|
|
168
196
|
footer={currentFooter}
|
|
169
197
|
header={currentHeader}
|
|
198
|
+
{...config}
|
|
170
199
|
onCancel={() => {
|
|
171
200
|
currentDialog.close(config.result);
|
|
172
201
|
}}
|
package/src/views/useDrawer.tsx
CHANGED
|
@@ -15,9 +15,11 @@ import { FlowViewContextProvider } from '../FlowContextProvider';
|
|
|
15
15
|
import { registerPopupVariable } from './createViewMeta';
|
|
16
16
|
import DrawerComponent from './DrawerComponent';
|
|
17
17
|
import usePatchElement from './usePatchElement';
|
|
18
|
+
import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } from './viewEvents';
|
|
18
19
|
import { FlowEngineProvider } from '../provider';
|
|
19
20
|
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
20
21
|
import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
|
|
22
|
+
import { runViewBeforeClose } from './runViewBeforeClose';
|
|
21
23
|
|
|
22
24
|
export function useDrawer() {
|
|
23
25
|
const holderRef = React.useRef(null);
|
|
@@ -54,6 +56,7 @@ export function useDrawer() {
|
|
|
54
56
|
RenderNestedDrawer.displayName = 'RenderNestedDrawer';
|
|
55
57
|
|
|
56
58
|
const open = (config, flowContext: FlowEngineContext) => {
|
|
59
|
+
const parentEngine = flowContext.engine;
|
|
57
60
|
const drawerRef = React.createRef<{
|
|
58
61
|
destroy: () => void;
|
|
59
62
|
update: (config: any) => void;
|
|
@@ -105,6 +108,8 @@ export function useDrawer() {
|
|
|
105
108
|
const ctx = new FlowContext();
|
|
106
109
|
// 为当前视图创建作用域引擎(隔离实例与缓存)
|
|
107
110
|
const scopedEngine = createViewScopedEngine(flowContext.engine);
|
|
111
|
+
const openerEngine = resolveOpenerEngine(parentEngine, scopedEngine);
|
|
112
|
+
|
|
108
113
|
// 先将引擎暴露给视图上下文,再按需继承父上下文
|
|
109
114
|
ctx.defineProperty('engine', { value: scopedEngine });
|
|
110
115
|
ctx.addDelegate(scopedEngine.context);
|
|
@@ -114,32 +119,48 @@ export function useDrawer() {
|
|
|
114
119
|
ctx.addDelegate(flowContext.engine.context);
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
// 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
|
|
123
|
+
let destroyed = false;
|
|
124
|
+
|
|
117
125
|
// 构造 currentDrawer 实例
|
|
118
126
|
const currentDrawer = {
|
|
119
127
|
type: 'drawer' as const,
|
|
120
128
|
inputArgs: config.inputArgs || {},
|
|
121
129
|
preventClose: !!config.preventClose,
|
|
130
|
+
beforeClose: undefined,
|
|
122
131
|
destroy: (result?: any) => {
|
|
132
|
+
if (destroyed) return;
|
|
133
|
+
destroyed = true;
|
|
123
134
|
config.onClose?.();
|
|
124
135
|
drawerRef.current?.destroy();
|
|
125
136
|
closeFunc?.();
|
|
126
137
|
resolvePromise?.(result);
|
|
138
|
+
// Notify opener view that it becomes active again.
|
|
139
|
+
const openerEmitter = openerEngine?.emitter;
|
|
140
|
+
bumpViewActivatedVersion(openerEmitter);
|
|
141
|
+
openerEmitter?.emit?.(VIEW_ACTIVATED_EVENT, { type: 'drawer', viewUid: currentDrawer?.inputArgs?.viewUid });
|
|
127
142
|
// 关闭时修正 previous/next 指针
|
|
128
143
|
scopedEngine.unlinkFromStack();
|
|
129
144
|
},
|
|
130
145
|
update: (newConfig) => drawerRef.current?.update(newConfig),
|
|
131
|
-
close: (result?: any, force?: boolean) => {
|
|
146
|
+
close: async (result?: any, force?: boolean) => {
|
|
132
147
|
if (config.preventClose && !force) {
|
|
133
|
-
return;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const shouldClose = await runViewBeforeClose(currentDrawer, { result, force });
|
|
152
|
+
if (!shouldClose) {
|
|
153
|
+
return false;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
|
|
137
157
|
// 交由路由系统来销毁当前视图
|
|
138
158
|
config.inputArgs.navigation.back();
|
|
139
|
-
return;
|
|
159
|
+
return true;
|
|
140
160
|
}
|
|
141
161
|
|
|
142
162
|
currentDrawer.destroy(result);
|
|
163
|
+
return true;
|
|
143
164
|
},
|
|
144
165
|
Footer: FooterComponent,
|
|
145
166
|
Header: HeaderComponent,
|
|
@@ -159,9 +180,17 @@ export function useDrawer() {
|
|
|
159
180
|
|
|
160
181
|
ctx.defineProperty('view', {
|
|
161
182
|
get: () => currentDrawer,
|
|
162
|
-
// meta: createViewMeta(ctx),
|
|
163
183
|
resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
|
|
164
184
|
});
|
|
185
|
+
// 注册视图销毁回调,供外部(如 afterSuccess)通过引擎栈遍历来关闭多层弹窗。
|
|
186
|
+
// 对路由触发的弹窗:先 navigation.back() 清理 URL(replace 方式),再 destroy() 立即移除元素;
|
|
187
|
+
// 对非路由弹窗:直接 destroy()。destroy() 已做幂等保护,FlowPage 后续清理不会重复执行。
|
|
188
|
+
scopedEngine.setDestroyView(() => {
|
|
189
|
+
if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
|
|
190
|
+
config.inputArgs.navigation.back();
|
|
191
|
+
}
|
|
192
|
+
currentDrawer.destroy();
|
|
193
|
+
});
|
|
165
194
|
// 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
|
|
166
195
|
registerPopupVariable(ctx, currentDrawer);
|
|
167
196
|
|