@nocobase/flow-engine 2.1.0-alpha.4 → 2.1.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/JSRunner.d.ts +10 -1
- package/lib/JSRunner.js +50 -5
- package/lib/ViewScopedFlowEngine.js +5 -1
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +613 -21
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +68 -10
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +16 -2
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/LazyDropdown.js +96 -39
- package/lib/components/subModel/index.d.ts +1 -0
- package/lib/components/subModel/index.js +19 -0
- package/lib/components/subModel/utils.d.ts +1 -1
- package/lib/components/subModel/utils.js +9 -3
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +75 -0
- package/lib/data-source/index.js +247 -5
- package/lib/executor/FlowExecutor.js +32 -9
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +3 -0
- package/lib/flowContext.js +43 -1
- package/lib/flowEngine.d.ts +151 -1
- package/lib/flowEngine.js +389 -15
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +13 -10
- package/lib/models/flowModel.js +78 -18
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +20 -12
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +5 -1
- package/lib/scheduler/ModelOperationScheduler.js +3 -2
- package/lib/types.d.ts +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.js +6 -2
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +20 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +20 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +302 -144
- package/package.json +6 -5
- package/src/JSRunner.ts +68 -4
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +27 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +82 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +16 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsPreprocessDefault.test.ts +23 -0
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +558 -3
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +758 -19
- package/src/components/dnd/index.tsx +305 -28
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +88 -10
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +18 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +189 -3
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/LazyDropdown.tsx +107 -43
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +319 -36
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +7 -1
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +68 -1
- package/src/data-source/index.ts +304 -6
- package/src/executor/FlowExecutor.ts +35 -10
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +47 -3
- package/src/flowEngine.ts +445 -11
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +214 -0
- package/src/models/__tests__/flowModel.test.ts +47 -3
- package/src/models/flowModel.tsx +119 -33
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +22 -12
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/scheduler/ModelOperationScheduler.ts +14 -3
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +5 -1
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/randomId.ts +48 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +6 -2
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +13 -12
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +25 -3
- package/src/views/useDrawer.tsx +25 -3
- package/src/views/usePage.tsx +365 -179
package/src/views/FlowView.tsx
CHANGED
|
@@ -11,13 +11,23 @@ import { PopoverProps as AntdPopoverProps } from 'antd';
|
|
|
11
11
|
import { FlowContext } from '../flowContext';
|
|
12
12
|
import { ViewNavigation } from './ViewNavigation';
|
|
13
13
|
|
|
14
|
+
export type FlowViewBeforeClosePayload = {
|
|
15
|
+
result?: any;
|
|
16
|
+
force?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type FlowViewBeforeCloseHandler = (
|
|
20
|
+
payload: FlowViewBeforeClosePayload,
|
|
21
|
+
) => Promise<boolean | void> | boolean | void;
|
|
22
|
+
|
|
14
23
|
export type FlowView = {
|
|
15
24
|
type: 'drawer' | 'popover' | 'dialog' | 'embed';
|
|
16
25
|
inputArgs: any;
|
|
17
26
|
Header: React.FC<{ title?: React.ReactNode; extra?: React.ReactNode }> | null;
|
|
18
27
|
Footer: React.FC<{ children?: React.ReactNode }> | null;
|
|
19
|
-
close: (result?: any, force?: boolean) => void;
|
|
28
|
+
close: (result?: any, force?: boolean) => Promise<boolean | void> | boolean | void;
|
|
20
29
|
update: (newConfig: any) => void;
|
|
30
|
+
beforeClose?: FlowViewBeforeCloseHandler;
|
|
21
31
|
navigation?: ViewNavigation;
|
|
22
32
|
/** 页面的销毁方法 */
|
|
23
33
|
destroy?: () => void;
|
|
@@ -74,11 +84,21 @@ export class FlowViewer {
|
|
|
74
84
|
if (this.types[type]) {
|
|
75
85
|
zIndex += 1;
|
|
76
86
|
const onClose = others.onClose;
|
|
87
|
+
let zIndexReleased = false;
|
|
88
|
+
const releaseZIndex = () => {
|
|
89
|
+
if (!zIndexReleased) {
|
|
90
|
+
zIndexReleased = true;
|
|
91
|
+
zIndex -= 1;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
77
94
|
const _zIndex = others.zIndex;
|
|
78
95
|
others.onClose = (...args) => {
|
|
79
96
|
onClose?.(...args);
|
|
80
|
-
|
|
97
|
+
releaseZIndex();
|
|
81
98
|
};
|
|
99
|
+
if (type === 'embed') {
|
|
100
|
+
others.onOpenCancelled = releaseZIndex;
|
|
101
|
+
}
|
|
82
102
|
// embed 不能设置过高的 zIndex,会遮挡菜单的折叠按钮图表
|
|
83
103
|
if (type !== 'embed') {
|
|
84
104
|
others.zIndex = _zIndex ?? this.getNextZIndex();
|
|
@@ -24,6 +24,7 @@ export const PageComponent = forwardRef((props: any, ref) => {
|
|
|
24
24
|
title: _title,
|
|
25
25
|
styles = {},
|
|
26
26
|
zIndex = 4, // 这个默认值是为了防止表格的阴影显示到子页面上面
|
|
27
|
+
onClose,
|
|
27
28
|
} = mergedProps;
|
|
28
29
|
const closedRef = useRef(false);
|
|
29
30
|
const flowEngine = useFlowEngine();
|
|
@@ -86,10 +87,12 @@ export const PageComponent = forwardRef((props: any, ref) => {
|
|
|
86
87
|
type="text"
|
|
87
88
|
size="small"
|
|
88
89
|
icon={<CloseOutlined />}
|
|
89
|
-
onClick={() => {
|
|
90
|
+
onClick={async () => {
|
|
90
91
|
if (!closedRef.current) {
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const closed = await onClose?.();
|
|
93
|
+
if (closed !== false) {
|
|
94
|
+
closedRef.current = true;
|
|
95
|
+
}
|
|
93
96
|
}
|
|
94
97
|
}}
|
|
95
98
|
style={{
|
|
@@ -111,7 +114,7 @@ export const PageComponent = forwardRef((props: any, ref) => {
|
|
|
111
114
|
{extra && <div>{extra}</div>}
|
|
112
115
|
</div>
|
|
113
116
|
);
|
|
114
|
-
}, [header, _title, flowEngine.context.themeToken, styles.header,
|
|
117
|
+
}, [header, _title, flowEngine.context.themeToken, styles.header, onClose]);
|
|
115
118
|
|
|
116
119
|
// Footer 组件
|
|
117
120
|
const FooterComponent = useMemo(() => {
|
|
@@ -26,6 +26,10 @@ function encodeFilterByTk(val: SharedViewParam['filterByTk']): string {
|
|
|
26
26
|
return encodeURIComponent(String(val));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function hasUsableSourceId(sourceId: unknown): sourceId is string | number {
|
|
30
|
+
return sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
/**
|
|
30
34
|
* 将 ViewParam 数组转换为 pathname
|
|
31
35
|
*
|
|
@@ -65,8 +69,8 @@ export function generatePathnameFromViewParams(viewParams: ViewParams[]): string
|
|
|
65
69
|
segments.push('filterbytk', encoded);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
|
-
if (viewParam.sourceId) {
|
|
69
|
-
segments.push('sourceid', viewParam.sourceId);
|
|
72
|
+
if (hasUsableSourceId(viewParam.sourceId)) {
|
|
73
|
+
segments.push('sourceid', String(viewParam.sourceId));
|
|
70
74
|
}
|
|
71
75
|
});
|
|
72
76
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { describe, expect, it, beforeEach } from 'vitest';
|
|
11
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
|
12
12
|
import { render, act, waitFor, screen } from '@testing-library/react';
|
|
13
13
|
import { FlowEngine } from '../../flowEngine';
|
|
14
14
|
import { FlowEngineProvider } from '../../provider';
|
|
@@ -167,15 +167,16 @@ describe('FlowViewer zIndex with usePage', () => {
|
|
|
167
167
|
);
|
|
168
168
|
|
|
169
169
|
await waitFor(() => expect(api).toBeDefined());
|
|
170
|
+
const pageApi = api as NonNullable<typeof api>;
|
|
170
171
|
|
|
171
172
|
await act(async () => {
|
|
172
|
-
|
|
173
|
+
pageApi.open({ target, content: <div data-testid="page1">Page 1</div> }, engine.context);
|
|
173
174
|
});
|
|
174
175
|
await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
|
|
175
176
|
|
|
176
177
|
// Opening page2 into the global embed container should destroy page1 (replace behavior).
|
|
177
178
|
await act(async () => {
|
|
178
|
-
|
|
179
|
+
pageApi.open({ target, content: <div data-testid="page2">Page 2</div> }, engine.context);
|
|
179
180
|
});
|
|
180
181
|
await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument());
|
|
181
182
|
expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
|
|
@@ -183,4 +184,243 @@ describe('FlowViewer zIndex with usePage', () => {
|
|
|
183
184
|
unmount();
|
|
184
185
|
document.body.removeChild(target);
|
|
185
186
|
});
|
|
187
|
+
|
|
188
|
+
it('keeps active global embed view when replacement beforeClose blocks closing', async () => {
|
|
189
|
+
let getViewer: () => FlowViewer;
|
|
190
|
+
const beforeClose = vi.fn().mockResolvedValue(false);
|
|
191
|
+
|
|
192
|
+
const target = document.createElement('div');
|
|
193
|
+
target.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
194
|
+
document.body.appendChild(target);
|
|
195
|
+
|
|
196
|
+
const { unmount } = render(
|
|
197
|
+
<Wrapper
|
|
198
|
+
onReady={(fn) => {
|
|
199
|
+
getViewer = fn;
|
|
200
|
+
}}
|
|
201
|
+
/>,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
await waitFor(() => expect(getViewer).toBeDefined());
|
|
205
|
+
const initialZIndex = getViewer().getNextZIndex();
|
|
206
|
+
|
|
207
|
+
let page1: any;
|
|
208
|
+
await act(async () => {
|
|
209
|
+
page1 = getViewer().embed({
|
|
210
|
+
target,
|
|
211
|
+
content: (currentPage) => {
|
|
212
|
+
currentPage.beforeClose = beforeClose;
|
|
213
|
+
return <div data-testid="page1">Page 1</div>;
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
|
|
219
|
+
expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1);
|
|
220
|
+
|
|
221
|
+
await act(async () => {
|
|
222
|
+
const page2 = getViewer().embed({ target, content: <div data-testid="page2">Page 2</div> });
|
|
223
|
+
await page2;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(beforeClose).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(screen.getByTestId('page1')).toBeInTheDocument();
|
|
228
|
+
expect(screen.queryByTestId('page2')).not.toBeInTheDocument();
|
|
229
|
+
expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1);
|
|
230
|
+
|
|
231
|
+
await act(async () => {
|
|
232
|
+
page1.destroy();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
unmount();
|
|
236
|
+
document.body.removeChild(target);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('opens the replacement view after async beforeClose allows global embed replacement', async () => {
|
|
240
|
+
let getViewer: () => FlowViewer;
|
|
241
|
+
const beforeClose = vi.fn().mockResolvedValue(true);
|
|
242
|
+
|
|
243
|
+
const target = document.createElement('div');
|
|
244
|
+
target.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
245
|
+
document.body.appendChild(target);
|
|
246
|
+
|
|
247
|
+
const { unmount } = render(
|
|
248
|
+
<Wrapper
|
|
249
|
+
onReady={(fn) => {
|
|
250
|
+
getViewer = fn;
|
|
251
|
+
}}
|
|
252
|
+
/>,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await waitFor(() => expect(getViewer).toBeDefined());
|
|
256
|
+
|
|
257
|
+
await act(async () => {
|
|
258
|
+
getViewer().embed({
|
|
259
|
+
target,
|
|
260
|
+
content: (currentPage) => {
|
|
261
|
+
currentPage.beforeClose = beforeClose;
|
|
262
|
+
return <div data-testid="page1">Page 1</div>;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
|
|
268
|
+
|
|
269
|
+
await act(async () => {
|
|
270
|
+
getViewer().embed({ target, content: <div data-testid="page2">Page 2</div> });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument());
|
|
274
|
+
expect(beforeClose).toHaveBeenCalledTimes(1);
|
|
275
|
+
expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
|
|
276
|
+
|
|
277
|
+
unmount();
|
|
278
|
+
document.body.removeChild(target);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('runs a pending close only once and allows retry when beforeClose rejects', async () => {
|
|
282
|
+
let getViewer: () => FlowViewer;
|
|
283
|
+
let resolveFirstClose: (value: boolean) => void;
|
|
284
|
+
const beforeClose = vi
|
|
285
|
+
.fn()
|
|
286
|
+
.mockImplementationOnce(() => new Promise<boolean>((resolve) => (resolveFirstClose = resolve)))
|
|
287
|
+
.mockResolvedValueOnce(true);
|
|
288
|
+
|
|
289
|
+
const { unmount } = render(
|
|
290
|
+
<Wrapper
|
|
291
|
+
onReady={(fn) => {
|
|
292
|
+
getViewer = fn;
|
|
293
|
+
}}
|
|
294
|
+
/>,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
await waitFor(() => expect(getViewer).toBeDefined());
|
|
298
|
+
|
|
299
|
+
let page: any;
|
|
300
|
+
await act(async () => {
|
|
301
|
+
page = getViewer().embed({
|
|
302
|
+
content: (currentPage) => {
|
|
303
|
+
currentPage.beforeClose = beforeClose;
|
|
304
|
+
return <div data-testid="draft-editor">Draft editor</div>;
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await waitFor(() => expect(screen.getByTestId('draft-editor')).toBeInTheDocument());
|
|
310
|
+
|
|
311
|
+
const firstClose = page.close();
|
|
312
|
+
const secondClose = page.close();
|
|
313
|
+
expect(firstClose).toBe(secondClose);
|
|
314
|
+
expect(beforeClose).toHaveBeenCalledTimes(1);
|
|
315
|
+
|
|
316
|
+
await act(async () => {
|
|
317
|
+
resolveFirstClose(false);
|
|
318
|
+
await firstClose;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(screen.getByTestId('draft-editor')).toBeInTheDocument();
|
|
322
|
+
|
|
323
|
+
await act(async () => {
|
|
324
|
+
await page.close();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(beforeClose).toHaveBeenCalledTimes(2);
|
|
328
|
+
await waitFor(() => expect(screen.queryByTestId('draft-editor')).not.toBeInTheDocument());
|
|
329
|
+
|
|
330
|
+
unmount();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('keeps only the latest pending global embed replacement', async () => {
|
|
334
|
+
let getViewer: () => FlowViewer;
|
|
335
|
+
let resolveBeforeClose: (value: boolean) => void;
|
|
336
|
+
const beforeClose = vi.fn(() => new Promise<boolean>((resolve) => (resolveBeforeClose = resolve)));
|
|
337
|
+
|
|
338
|
+
const target = document.createElement('div');
|
|
339
|
+
target.id = GLOBAL_EMBED_CONTAINER_ID;
|
|
340
|
+
document.body.appendChild(target);
|
|
341
|
+
|
|
342
|
+
const { unmount } = render(
|
|
343
|
+
<Wrapper
|
|
344
|
+
onReady={(fn) => {
|
|
345
|
+
getViewer = fn;
|
|
346
|
+
}}
|
|
347
|
+
/>,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
await waitFor(() => expect(getViewer).toBeDefined());
|
|
351
|
+
const initialZIndex = getViewer().getNextZIndex();
|
|
352
|
+
|
|
353
|
+
await act(async () => {
|
|
354
|
+
getViewer().embed({
|
|
355
|
+
target,
|
|
356
|
+
content: (currentPage) => {
|
|
357
|
+
currentPage.beforeClose = beforeClose;
|
|
358
|
+
return <div data-testid="page1">Page 1</div>;
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
|
|
364
|
+
|
|
365
|
+
const page2 = getViewer().embed({ target, content: <div data-testid="page2">Page 2</div> });
|
|
366
|
+
const page3 = getViewer().embed({ target, content: <div data-testid="page3">Page 3</div> });
|
|
367
|
+
|
|
368
|
+
await act(async () => {
|
|
369
|
+
resolveBeforeClose(true);
|
|
370
|
+
await page2;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(beforeClose).toHaveBeenCalledTimes(1);
|
|
374
|
+
expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
|
|
375
|
+
expect(screen.queryByTestId('page2')).not.toBeInTheDocument();
|
|
376
|
+
await waitFor(() => expect(screen.getByTestId('page3')).toBeInTheDocument());
|
|
377
|
+
expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1);
|
|
378
|
+
|
|
379
|
+
unmount();
|
|
380
|
+
document.body.removeChild(target);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('keeps the embed close button usable after beforeClose blocks closing', async () => {
|
|
384
|
+
let getViewer: () => FlowViewer;
|
|
385
|
+
const beforeClose = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
|
386
|
+
|
|
387
|
+
const { unmount } = render(
|
|
388
|
+
<Wrapper
|
|
389
|
+
onReady={(fn) => {
|
|
390
|
+
getViewer = fn;
|
|
391
|
+
}}
|
|
392
|
+
/>,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
await waitFor(() => expect(getViewer).toBeDefined());
|
|
396
|
+
|
|
397
|
+
await act(async () => {
|
|
398
|
+
getViewer().embed({
|
|
399
|
+
title: 'Draft editor',
|
|
400
|
+
content: (currentPage) => {
|
|
401
|
+
currentPage.beforeClose = beforeClose;
|
|
402
|
+
return <div data-testid="draft-editor">Draft editor</div>;
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await waitFor(() => expect(screen.getByTestId('draft-editor')).toBeInTheDocument());
|
|
408
|
+
const closeButton = screen.getByRole('button');
|
|
409
|
+
|
|
410
|
+
await act(async () => {
|
|
411
|
+
closeButton.click();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(beforeClose).toHaveBeenCalledTimes(1);
|
|
415
|
+
expect(screen.getByTestId('draft-editor')).toBeInTheDocument();
|
|
416
|
+
|
|
417
|
+
await act(async () => {
|
|
418
|
+
closeButton.click();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(beforeClose).toHaveBeenCalledTimes(2);
|
|
422
|
+
await waitFor(() => expect(screen.queryByTestId('draft-editor')).not.toBeInTheDocument());
|
|
423
|
+
|
|
424
|
+
unmount();
|
|
425
|
+
});
|
|
186
426
|
});
|
|
@@ -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
|
+
});
|
|
@@ -26,6 +26,7 @@ vi.mock('../../ViewScopedFlowEngine', () => ({
|
|
|
26
26
|
createViewScopedEngine: (engine) => ({
|
|
27
27
|
context: new FlowContext(),
|
|
28
28
|
unlinkFromStack: vi.fn(),
|
|
29
|
+
setDestroyView: vi.fn(),
|
|
29
30
|
// mimic real view stack linkage: previousEngine points to the last engine in chain
|
|
30
31
|
previousEngine: (engine as any)?.nextEngine || engine,
|
|
31
32
|
}),
|
|
@@ -75,40 +76,40 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
75
76
|
return api;
|
|
76
77
|
};
|
|
77
78
|
|
|
78
|
-
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 () => {
|
|
79
80
|
const api = renderUseDialog();
|
|
80
81
|
const flowContext = createMockFlowContext();
|
|
81
82
|
|
|
82
83
|
const dialog = api.open({}, flowContext);
|
|
83
84
|
|
|
84
|
-
dialog.close();
|
|
85
|
+
await dialog.close();
|
|
85
86
|
|
|
86
87
|
expect(mockCloseFunc).toHaveBeenCalled();
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
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 () => {
|
|
90
91
|
const api = renderUseDialog();
|
|
91
92
|
const flowContext = createMockFlowContext();
|
|
92
93
|
|
|
93
94
|
const dialog = api.open({ preventClose: true }, flowContext);
|
|
94
95
|
|
|
95
|
-
dialog.close();
|
|
96
|
+
await dialog.close();
|
|
96
97
|
|
|
97
98
|
expect(mockCloseFunc).not.toHaveBeenCalled();
|
|
98
99
|
});
|
|
99
100
|
|
|
100
|
-
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 () => {
|
|
101
102
|
const api = renderUseDialog();
|
|
102
103
|
const flowContext = createMockFlowContext();
|
|
103
104
|
|
|
104
105
|
const dialog = api.open({ preventClose: true }, flowContext);
|
|
105
106
|
|
|
106
|
-
dialog.close(undefined, true);
|
|
107
|
+
await dialog.close(undefined, true);
|
|
107
108
|
|
|
108
109
|
expect(mockCloseFunc).toHaveBeenCalled();
|
|
109
110
|
});
|
|
110
111
|
|
|
111
|
-
it('should delegate to navigation.back when triggerByRouter is true', () => {
|
|
112
|
+
it('should delegate to navigation.back when triggerByRouter is true', async () => {
|
|
112
113
|
const api = renderUseDialog();
|
|
113
114
|
const flowContext = createMockFlowContext();
|
|
114
115
|
const backMock = vi.fn();
|
|
@@ -125,25 +126,25 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
125
126
|
flowContext,
|
|
126
127
|
);
|
|
127
128
|
|
|
128
|
-
dialog.close();
|
|
129
|
+
await dialog.close();
|
|
129
130
|
|
|
130
131
|
expect(backMock).toHaveBeenCalled();
|
|
131
132
|
// Should not call destroy directly, let router handle it
|
|
132
133
|
expect(mockCloseFunc).not.toHaveBeenCalled();
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
it('should emit view activated event on opener engine', () => {
|
|
136
|
+
it('should emit view activated event on opener engine', async () => {
|
|
136
137
|
const api = renderUseDialog();
|
|
137
138
|
const flowContext = createMockFlowContext();
|
|
138
139
|
const emitSpy = flowContext.engine.emitter.emit;
|
|
139
140
|
|
|
140
141
|
const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
|
|
141
142
|
|
|
142
|
-
dialog.close();
|
|
143
|
+
await dialog.close();
|
|
143
144
|
expect(emitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
|
|
144
145
|
});
|
|
145
146
|
|
|
146
|
-
it('should emit view events on immediate opener engine (previousEngine) when present', () => {
|
|
147
|
+
it('should emit view events on immediate opener engine (previousEngine) when present', async () => {
|
|
147
148
|
const api = renderUseDialog();
|
|
148
149
|
const flowContext = createMockFlowContext();
|
|
149
150
|
const rootEmitSpy = flowContext.engine.emitter.emit;
|
|
@@ -152,7 +153,7 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
152
153
|
|
|
153
154
|
const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
|
|
154
155
|
|
|
155
|
-
dialog.close();
|
|
156
|
+
await dialog.close();
|
|
156
157
|
expect(openerEmitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
|
|
157
158
|
expect(rootEmitSpy).not.toHaveBeenCalledWith('view:activated', expect.anything());
|
|
158
159
|
});
|
|
@@ -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
|
@@ -19,6 +19,7 @@ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } f
|
|
|
19
19
|
import { FlowEngineProvider } from '../provider';
|
|
20
20
|
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
21
21
|
import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
|
|
22
|
+
import { runViewBeforeClose } from './runViewBeforeClose';
|
|
22
23
|
|
|
23
24
|
let uuid = 0;
|
|
24
25
|
|
|
@@ -89,12 +90,18 @@ export function useDialog() {
|
|
|
89
90
|
ctx.addDelegate(flowContext.engine.context);
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
|
|
94
|
+
let destroyed = false;
|
|
95
|
+
|
|
92
96
|
// 构造 currentDialog 实例
|
|
93
97
|
const currentDialog = {
|
|
94
98
|
type: 'dialog' as const,
|
|
95
99
|
inputArgs: config.inputArgs || {},
|
|
96
100
|
preventClose: !!config.preventClose,
|
|
101
|
+
beforeClose: undefined,
|
|
97
102
|
destroy: (result?: any) => {
|
|
103
|
+
if (destroyed) return;
|
|
104
|
+
destroyed = true;
|
|
98
105
|
config.onClose?.();
|
|
99
106
|
dialogRef.current?.destroy();
|
|
100
107
|
closeFunc?.();
|
|
@@ -107,18 +114,24 @@ export function useDialog() {
|
|
|
107
114
|
scopedEngine.unlinkFromStack();
|
|
108
115
|
},
|
|
109
116
|
update: (newConfig) => dialogRef.current?.update(newConfig),
|
|
110
|
-
close: (result?: any, force?: boolean) => {
|
|
117
|
+
close: async (result?: any, force?: boolean) => {
|
|
111
118
|
if (config.preventClose && !force) {
|
|
112
|
-
return;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const shouldClose = await runViewBeforeClose(currentDialog, { result, force });
|
|
123
|
+
if (!shouldClose) {
|
|
124
|
+
return false;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
|
|
116
128
|
// 交由路由系统来销毁当前视图
|
|
117
129
|
config.inputArgs.navigation.back();
|
|
118
|
-
return;
|
|
130
|
+
return true;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
currentDialog.destroy(result);
|
|
134
|
+
return true;
|
|
122
135
|
},
|
|
123
136
|
Footer: FooterComponent,
|
|
124
137
|
Header: HeaderComponent,
|
|
@@ -140,6 +153,15 @@ export function useDialog() {
|
|
|
140
153
|
get: () => currentDialog,
|
|
141
154
|
resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
|
|
142
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
|
+
});
|
|
143
165
|
// 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
|
|
144
166
|
registerPopupVariable(ctx, currentDialog);
|
|
145
167
|
// 内部组件,在 Provider 内部计算 content
|
package/src/views/useDrawer.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { VIEW_ACTIVATED_EVENT, bumpViewActivatedVersion, resolveOpenerEngine } f
|
|
|
19
19
|
import { FlowEngineProvider } from '../provider';
|
|
20
20
|
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
21
21
|
import { createViewRecordResolveOnServer, getViewRecordFromParent } from '../utils/variablesParams';
|
|
22
|
+
import { runViewBeforeClose } from './runViewBeforeClose';
|
|
22
23
|
|
|
23
24
|
export function useDrawer() {
|
|
24
25
|
const holderRef = React.useRef(null);
|
|
@@ -118,12 +119,18 @@ export function useDrawer() {
|
|
|
118
119
|
ctx.addDelegate(flowContext.engine.context);
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// 幂等保护:防止 FlowPage 路由清理时二次调用 destroy
|
|
123
|
+
let destroyed = false;
|
|
124
|
+
|
|
121
125
|
// 构造 currentDrawer 实例
|
|
122
126
|
const currentDrawer = {
|
|
123
127
|
type: 'drawer' as const,
|
|
124
128
|
inputArgs: config.inputArgs || {},
|
|
125
129
|
preventClose: !!config.preventClose,
|
|
130
|
+
beforeClose: undefined,
|
|
126
131
|
destroy: (result?: any) => {
|
|
132
|
+
if (destroyed) return;
|
|
133
|
+
destroyed = true;
|
|
127
134
|
config.onClose?.();
|
|
128
135
|
drawerRef.current?.destroy();
|
|
129
136
|
closeFunc?.();
|
|
@@ -136,18 +143,24 @@ export function useDrawer() {
|
|
|
136
143
|
scopedEngine.unlinkFromStack();
|
|
137
144
|
},
|
|
138
145
|
update: (newConfig) => drawerRef.current?.update(newConfig),
|
|
139
|
-
close: (result?: any, force?: boolean) => {
|
|
146
|
+
close: async (result?: any, force?: boolean) => {
|
|
140
147
|
if (config.preventClose && !force) {
|
|
141
|
-
return;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const shouldClose = await runViewBeforeClose(currentDrawer, { result, force });
|
|
152
|
+
if (!shouldClose) {
|
|
153
|
+
return false;
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
if (config.triggerByRouter && config.inputArgs?.navigation?.back) {
|
|
145
157
|
// 交由路由系统来销毁当前视图
|
|
146
158
|
config.inputArgs.navigation.back();
|
|
147
|
-
return;
|
|
159
|
+
return true;
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
currentDrawer.destroy(result);
|
|
163
|
+
return true;
|
|
151
164
|
},
|
|
152
165
|
Footer: FooterComponent,
|
|
153
166
|
Header: HeaderComponent,
|
|
@@ -169,6 +182,15 @@ export function useDrawer() {
|
|
|
169
182
|
get: () => currentDrawer,
|
|
170
183
|
resolveOnServer: createViewRecordResolveOnServer(ctx, () => getViewRecordFromParent(flowContext, ctx)),
|
|
171
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
|
+
});
|
|
172
194
|
// 顶层 popup 变量:弹窗记录/数据源/上级弹窗链(去重封装)
|
|
173
195
|
registerPopupVariable(ctx, currentDrawer);
|
|
174
196
|
|