@nocobase/flow-engine 2.0.51 → 2.0.53
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/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 +1 -0
- package/lib/flowContext.js +12 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/usePage.d.ts +4 -11
- package/lib/views/usePage.js +301 -150
- package/package.json +4 -4
- package/src/__tests__/flowContext.test.ts +17 -0
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +2 -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 +10 -0
- package/src/index.ts +2 -0
- package/src/views/FlowView.tsx +11 -1
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/usePage.tsx +364 -187
package/src/flowContext.ts
CHANGED
|
@@ -3034,6 +3034,7 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
3034
3034
|
declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
|
|
3035
3035
|
declare engine: FlowEngine;
|
|
3036
3036
|
declare api: APIClient;
|
|
3037
|
+
declare locale: string;
|
|
3037
3038
|
declare viewer: FlowViewer;
|
|
3038
3039
|
declare view: FlowView;
|
|
3039
3040
|
declare modal: HookAPI;
|
|
@@ -3144,6 +3145,15 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3144
3145
|
this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
|
|
3145
3146
|
return i18n.translate(keyOrTemplate, options);
|
|
3146
3147
|
});
|
|
3148
|
+
this.defineProperty('locale', {
|
|
3149
|
+
get: () => this.api?.auth?.locale || this.i18n?.language,
|
|
3150
|
+
cache: false,
|
|
3151
|
+
meta: Object.assign(() => ({ type: 'string', title: this.t('Current language'), sort: 970 }), {
|
|
3152
|
+
title: escapeT('Current language'),
|
|
3153
|
+
sort: 970,
|
|
3154
|
+
hasChildren: false,
|
|
3155
|
+
}),
|
|
3156
|
+
});
|
|
3147
3157
|
this.defineMethod('renderJson', function (template: any) {
|
|
3148
3158
|
return this.resolveJsonTemplate(template);
|
|
3149
3159
|
});
|
package/src/index.ts
CHANGED
|
@@ -57,5 +57,7 @@ export {
|
|
|
57
57
|
} from './views/viewEvents';
|
|
58
58
|
|
|
59
59
|
export * from './FlowDefinition';
|
|
60
|
+
export { DetachedFlowRegistry, replaceFlowRegistry, serializeFlowRegistry } from './flow-registry';
|
|
61
|
+
export type { FlowRegistryData } from './flow-registry';
|
|
60
62
|
export { createViewScopedEngine } from './ViewScopedFlowEngine';
|
|
61
63
|
export { createBlockScopedEngine } from './BlockScopedFlowEngine';
|
package/src/views/FlowView.tsx
CHANGED
|
@@ -84,11 +84,21 @@ export class FlowViewer {
|
|
|
84
84
|
if (this.types[type]) {
|
|
85
85
|
zIndex += 1;
|
|
86
86
|
const onClose = others.onClose;
|
|
87
|
+
let zIndexReleased = false;
|
|
88
|
+
const releaseZIndex = () => {
|
|
89
|
+
if (!zIndexReleased) {
|
|
90
|
+
zIndexReleased = true;
|
|
91
|
+
zIndex -= 1;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
87
94
|
const _zIndex = others.zIndex;
|
|
88
95
|
others.onClose = (...args) => {
|
|
89
96
|
onClose?.(...args);
|
|
90
|
-
|
|
97
|
+
releaseZIndex();
|
|
91
98
|
};
|
|
99
|
+
if (type === 'embed') {
|
|
100
|
+
others.onOpenCancelled = releaseZIndex;
|
|
101
|
+
}
|
|
92
102
|
// embed 不能设置过高的 zIndex,会遮挡菜单的折叠按钮图表
|
|
93
103
|
if (type !== 'embed') {
|
|
94
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(() => {
|
|
@@ -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
|
});
|