@nocobase/flow-engine 2.1.0-beta.9 → 2.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +607 -19
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +12 -1
- package/lib/components/subModel/LazyDropdown.js +301 -52
- package/lib/components/subModel/index.d.ts +1 -0
- package/lib/components/subModel/index.js +19 -0
- package/lib/components/subModel/utils.d.ts +2 -1
- package/lib/components/subModel/utils.js +15 -5
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +84 -0
- package/lib/data-source/index.js +269 -7
- package/lib/executor/FlowExecutor.js +6 -3
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +9 -1
- package/lib/flowContext.js +77 -6
- package/lib/flowEngine.d.ts +136 -4
- package/lib/flowEngine.js +429 -51
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +13 -10
- package/lib/models/flowModel.js +126 -34
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
- package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/base.js +464 -29
- package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
- package/lib/runjs-context/contexts/elementDoc.js +152 -0
- package/lib/runjs-context/setup.js +1 -0
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/types.d.ts +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/loadedPageCache.d.ts +24 -0
- package/lib/utils/loadedPageCache.js +139 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +28 -4
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.d.ts +12 -2
- package/lib/views/ViewNavigation.js +28 -9
- package/lib/views/createViewMeta.js +114 -50
- package/lib/views/inheritLayoutContext.d.ts +10 -0
- package/lib/views/inheritLayoutContext.js +50 -0
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +12 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +12 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +5 -4
- package/src/FlowContextProvider.tsx +9 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +105 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
- package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +21 -0
- package/src/__tests__/runjsContextImplementations.test.ts +9 -2
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsLocales.test.ts +6 -5
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +750 -17
- package/src/components/dnd/index.tsx +305 -28
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +16 -2
- package/src/components/subModel/LazyDropdown.tsx +341 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +13 -2
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +69 -2
- package/src/data-source/index.ts +332 -8
- package/src/executor/FlowExecutor.ts +6 -3
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +85 -6
- package/src/flowEngine.ts +484 -45
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +65 -37
- package/src/models/flowModel.tsx +184 -65
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
- package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/base.ts +467 -31
- package/src/runjs-context/contexts/elementDoc.ts +130 -0
- package/src/runjs-context/setup.ts +1 -0
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +5 -1
- package/src/utils/loadedPageCache.ts +147 -0
- package/src/utils/parsePathnameToViewParams.ts +45 -5
- package/src/utils/randomId.ts +48 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +46 -9
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +13 -3
- package/src/views/useDrawer.tsx +13 -3
- package/src/views/usePage.tsx +367 -180
|
@@ -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
|
});
|
|
@@ -146,6 +146,47 @@ describe('ViewNavigation', () => {
|
|
|
146
146
|
|
|
147
147
|
expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin', { replace: true });
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
it('should use explicit basePath when navigating back', () => {
|
|
151
|
+
viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }], { basePath: '/embed' });
|
|
152
|
+
|
|
153
|
+
viewNavigation.back();
|
|
154
|
+
|
|
155
|
+
expect(mockCtx.router.navigate).toHaveBeenCalledWith('/embed', { replace: true });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should use layout route basePathname when explicit basePath is absent', () => {
|
|
159
|
+
mockCtx.layoutRoute = {
|
|
160
|
+
basePathname: '/mobile',
|
|
161
|
+
};
|
|
162
|
+
viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }]);
|
|
163
|
+
|
|
164
|
+
viewNavigation.back();
|
|
165
|
+
|
|
166
|
+
expect(mockCtx.router.navigate).toHaveBeenCalledWith('/mobile', { replace: true });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should fall back to layout routePath when explicit basePath is absent', () => {
|
|
170
|
+
mockCtx.layout = {
|
|
171
|
+
routePath: '/mobile',
|
|
172
|
+
};
|
|
173
|
+
viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }]);
|
|
174
|
+
|
|
175
|
+
viewNavigation.back();
|
|
176
|
+
|
|
177
|
+
expect(mockCtx.router.navigate).toHaveBeenCalledWith('/mobile', { replace: true });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should ignore relative layout routePath when runtime basePathname is absent', () => {
|
|
181
|
+
mockCtx.layout = {
|
|
182
|
+
routePath: 'public-forms',
|
|
183
|
+
};
|
|
184
|
+
viewNavigation = new ViewNavigation(mockCtx, [{ viewUid: 'view1' }]);
|
|
185
|
+
|
|
186
|
+
viewNavigation.back();
|
|
187
|
+
|
|
188
|
+
expect(mockCtx.router.navigate).toHaveBeenCalledWith('/admin', { replace: true });
|
|
189
|
+
});
|
|
149
190
|
});
|
|
150
191
|
});
|
|
151
192
|
|
|
@@ -160,6 +201,17 @@ describe('generatePathnameFromViewParams', () => {
|
|
|
160
201
|
expect(generatePathnameFromViewParams([{ viewUid: 'xxx' }])).toBe('/admin/xxx');
|
|
161
202
|
});
|
|
162
203
|
|
|
204
|
+
it('should generate path with custom prefix', () => {
|
|
205
|
+
expect(generatePathnameFromViewParams([{ viewUid: 'xxx' }], { basePath: '/embed' })).toBe('/embed/xxx');
|
|
206
|
+
expect(generatePathnameFromViewParams([], { basePath: '/embed' })).toBe('/embed');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should generate path with nested basePath', () => {
|
|
210
|
+
expect(generatePathnameFromViewParams([{ viewUid: 'xxx' }], { basePath: '/admin/settings/public-forms' })).toBe(
|
|
211
|
+
'/admin/settings/public-forms/xxx',
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
163
215
|
it('should generate view with tab', () => {
|
|
164
216
|
expect(generatePathnameFromViewParams([{ viewUid: 'xxx', tabUid: 'yyy' }])).toBe('/admin/xxx/tab/yyy');
|
|
165
217
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { FlowContext } from '../../flowContext';
|
|
12
|
+
import { inheritLayoutContextForDetachedView } from '../inheritLayoutContext';
|
|
13
|
+
|
|
14
|
+
describe('inheritLayoutContextForDetachedView', () => {
|
|
15
|
+
it('inherits layout context for detached view contexts', () => {
|
|
16
|
+
const sourceContext = new FlowContext();
|
|
17
|
+
const engineContext = new FlowContext();
|
|
18
|
+
const layoutContext = new FlowContext();
|
|
19
|
+
const viewContext = new FlowContext();
|
|
20
|
+
|
|
21
|
+
engineContext.defineProperty('skipAclCheck', { value: false });
|
|
22
|
+
layoutContext.defineProperty('skipAclCheck', { value: true });
|
|
23
|
+
sourceContext.defineProperty('engine', {
|
|
24
|
+
value: {
|
|
25
|
+
context: engineContext,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
sourceContext.defineProperty('layoutContext', { value: layoutContext });
|
|
29
|
+
|
|
30
|
+
viewContext.addDelegate(engineContext);
|
|
31
|
+
inheritLayoutContextForDetachedView(viewContext, sourceContext);
|
|
32
|
+
|
|
33
|
+
expect(viewContext.skipAclCheck).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does nothing when source context has no layout context', () => {
|
|
37
|
+
const sourceContext = new FlowContext();
|
|
38
|
+
const engineContext = new FlowContext();
|
|
39
|
+
const viewContext = new FlowContext();
|
|
40
|
+
|
|
41
|
+
engineContext.defineProperty('skipAclCheck', { value: false });
|
|
42
|
+
sourceContext.defineProperty('engine', {
|
|
43
|
+
value: {
|
|
44
|
+
context: engineContext,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
viewContext.addDelegate(engineContext);
|
|
49
|
+
inheritLayoutContextForDetachedView(viewContext, sourceContext);
|
|
50
|
+
|
|
51
|
+
expect(viewContext.skipAclCheck).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -76,40 +76,40 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
76
76
|
return api;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
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 () => {
|
|
80
80
|
const api = renderUseDialog();
|
|
81
81
|
const flowContext = createMockFlowContext();
|
|
82
82
|
|
|
83
83
|
const dialog = api.open({}, flowContext);
|
|
84
84
|
|
|
85
|
-
dialog.close();
|
|
85
|
+
await dialog.close();
|
|
86
86
|
|
|
87
87
|
expect(mockCloseFunc).toHaveBeenCalled();
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
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 () => {
|
|
91
91
|
const api = renderUseDialog();
|
|
92
92
|
const flowContext = createMockFlowContext();
|
|
93
93
|
|
|
94
94
|
const dialog = api.open({ preventClose: true }, flowContext);
|
|
95
95
|
|
|
96
|
-
dialog.close();
|
|
96
|
+
await dialog.close();
|
|
97
97
|
|
|
98
98
|
expect(mockCloseFunc).not.toHaveBeenCalled();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
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 () => {
|
|
102
102
|
const api = renderUseDialog();
|
|
103
103
|
const flowContext = createMockFlowContext();
|
|
104
104
|
|
|
105
105
|
const dialog = api.open({ preventClose: true }, flowContext);
|
|
106
106
|
|
|
107
|
-
dialog.close(undefined, true);
|
|
107
|
+
await dialog.close(undefined, true);
|
|
108
108
|
|
|
109
109
|
expect(mockCloseFunc).toHaveBeenCalled();
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
it('should delegate to navigation.back when triggerByRouter is true', () => {
|
|
112
|
+
it('should delegate to navigation.back when triggerByRouter is true', async () => {
|
|
113
113
|
const api = renderUseDialog();
|
|
114
114
|
const flowContext = createMockFlowContext();
|
|
115
115
|
const backMock = vi.fn();
|
|
@@ -126,25 +126,25 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
126
126
|
flowContext,
|
|
127
127
|
);
|
|
128
128
|
|
|
129
|
-
dialog.close();
|
|
129
|
+
await dialog.close();
|
|
130
130
|
|
|
131
131
|
expect(backMock).toHaveBeenCalled();
|
|
132
132
|
// Should not call destroy directly, let router handle it
|
|
133
133
|
expect(mockCloseFunc).not.toHaveBeenCalled();
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
it('should emit view activated event on opener engine', () => {
|
|
136
|
+
it('should emit view activated event on opener engine', async () => {
|
|
137
137
|
const api = renderUseDialog();
|
|
138
138
|
const flowContext = createMockFlowContext();
|
|
139
139
|
const emitSpy = flowContext.engine.emitter.emit;
|
|
140
140
|
|
|
141
141
|
const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
|
|
142
142
|
|
|
143
|
-
dialog.close();
|
|
143
|
+
await dialog.close();
|
|
144
144
|
expect(emitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
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 () => {
|
|
148
148
|
const api = renderUseDialog();
|
|
149
149
|
const flowContext = createMockFlowContext();
|
|
150
150
|
const rootEmitSpy = flowContext.engine.emitter.emit;
|
|
@@ -153,7 +153,7 @@ describe('useDialog - close/destroy logic', () => {
|
|
|
153
153
|
|
|
154
154
|
const dialog = api.open({ inputArgs: { viewUid: 'child-view' } }, flowContext);
|
|
155
155
|
|
|
156
|
-
dialog.close();
|
|
156
|
+
await dialog.close();
|
|
157
157
|
expect(openerEmitSpy).toHaveBeenCalledWith('view:activated', expect.anything());
|
|
158
158
|
expect(rootEmitSpy).not.toHaveBeenCalledWith('view:activated', expect.anything());
|
|
159
159
|
});
|