@nocobase/flow-engine 2.2.0-beta.1 → 2.2.0-beta.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/lib/JSRunner.d.ts +1 -0
- package/lib/JSRunner.js +110 -20
- package/lib/components/FieldModelRenderer.js +44 -31
- package/lib/components/MobilePopup.js +28 -10
- package/lib/components/MobilePopup.style.js +27 -6
- package/lib/components/dnd/index.js +9 -2
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +86 -32
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +20 -0
- package/lib/flowContext.d.ts +1 -1
- package/lib/flowContext.js +97 -42
- package/lib/flowEngine.js +6 -0
- package/lib/flowSettings.d.ts +5 -1
- package/lib/flowSettings.js +70 -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/resources/apiResource.js +2 -1
- package/lib/resources/baseRecordResource.js +6 -17
- package/lib/resources/multiRecordResource.js +13 -3
- package/lib/resources/singleRecordResource.js +7 -2
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/types.d.ts +12 -0
- package/lib/utils/dataSourceDirty.d.ts +20 -0
- package/lib/utils/dataSourceDirty.js +139 -0
- package/lib/utils/dirtyAwareApiClient.d.ts +11 -0
- package/lib/utils/dirtyAwareApiClient.js +378 -0
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +11 -11
- package/lib/utils/loadedPageCache.d.ts +1 -0
- package/lib/utils/loadedPageCache.js +6 -0
- package/lib/utils/openViewRouteState.d.ts +28 -0
- package/lib/utils/openViewRouteState.js +125 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +3 -0
- package/lib/utils/parsePathnameToViewParams.js +18 -1
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/lib/views/ViewNavigation.js +5 -0
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowContext.test.ts +154 -0
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowSettings.test.ts +72 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
- package/src/__tests__/viewScopedFlowEngine.test.ts +72 -6
- package/src/components/FieldModelRenderer.tsx +50 -36
- package/src/components/MobilePopup.style.ts +34 -7
- package/src/components/MobilePopup.tsx +30 -10
- package/src/components/__tests__/FieldModelRenderer.test.tsx +165 -0
- package/src/components/__tests__/MobilePopup.style.test.tsx +103 -0
- package/src/components/__tests__/MobilePopup.test.tsx +109 -0
- package/src/components/dnd/index.tsx +11 -2
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +105 -35
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +381 -12
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +28 -0
- package/src/flowContext.ts +104 -36
- package/src/flowEngine.ts +6 -0
- package/src/flowSettings.ts +85 -1
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/resources/apiResource.ts +2 -1
- package/src/resources/baseRecordResource.ts +6 -23
- package/src/resources/multiRecordResource.ts +13 -3
- package/src/resources/singleRecordResource.ts +6 -1
- package/src/runjs-context/helpers.ts +12 -6
- package/src/types.ts +14 -0
- package/src/utils/__tests__/dirtyAwareApiClient.test.ts +392 -0
- package/src/utils/__tests__/openViewRouteState.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +36 -0
- package/src/utils/dataSourceDirty.ts +126 -0
- package/src/utils/dirtyAwareApiClient.ts +430 -0
- package/src/utils/index.ts +10 -9
- package/src/utils/loadedPageCache.ts +7 -0
- package/src/utils/openViewRouteState.ts +107 -0
- package/src/utils/parsePathnameToViewParams.ts +23 -1
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/src/views/ViewNavigation.ts +6 -1
- package/src/views/__tests__/ViewNavigation.test.ts +15 -0
- package/lib/utils/safeGlobals.d.ts +0 -28
- package/lib/utils/safeGlobals.js +0 -367
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
- package/src/utils/__tests__/safeGlobals.test.ts +0 -106
- package/src/utils/safeGlobals.ts +0 -406
|
@@ -11,6 +11,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
11
11
|
import { FlowEngine } from '../flowEngine';
|
|
12
12
|
import { MultiRecordResource } from '../resources/multiRecordResource';
|
|
13
13
|
import { SingleRecordResource } from '../resources/singleRecordResource';
|
|
14
|
+
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
14
15
|
|
|
15
16
|
describe('FlowEngine dataSource dirty registry', () => {
|
|
16
17
|
it('tracks versions per dataSourceKey + resourceName', () => {
|
|
@@ -60,4 +61,54 @@ describe('FlowEngine dataSource dirty registry', () => {
|
|
|
60
61
|
// plus root collection (safety)
|
|
61
62
|
expect(markSpy).toHaveBeenCalledWith('main', 'users');
|
|
62
63
|
});
|
|
64
|
+
|
|
65
|
+
it('marks dirty once for record write helpers when using the dirty-aware context api', async () => {
|
|
66
|
+
const engine = new FlowEngine();
|
|
67
|
+
const request = vi.fn(async () => ({ data: { data: { id: 1 }, meta: {} } }));
|
|
68
|
+
const dirtyEvents: Array<{ dataSourceKey: string; resourceNames: string[] }> = [];
|
|
69
|
+
engine.context.defineProperty('api', {
|
|
70
|
+
value: {
|
|
71
|
+
auth: { locale: 'zh-CN' },
|
|
72
|
+
request,
|
|
73
|
+
resource: vi.fn(),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
engine.emitter.on(DATA_SOURCE_DIRTY_EVENT, (event) => dirtyEvents.push(event));
|
|
77
|
+
|
|
78
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
79
|
+
multi.setDataSourceKey('main').setResourceName('posts');
|
|
80
|
+
await multi.create({ title: 't' } as any, { refresh: false });
|
|
81
|
+
|
|
82
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
84
|
+
expect(dirtyEvents).toEqual([{ dataSourceKey: 'main', resourceNames: ['posts'] }]);
|
|
85
|
+
|
|
86
|
+
const single = engine.createResource(SingleRecordResource);
|
|
87
|
+
single.setDataSourceKey('main').setResourceName('posts').setFilterByTk(1);
|
|
88
|
+
await single.save({ title: 'u' } as any, { refresh: false });
|
|
89
|
+
|
|
90
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
91
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(2);
|
|
92
|
+
expect(dirtyEvents).toEqual([
|
|
93
|
+
{ dataSourceKey: 'main', resourceNames: ['posts'] },
|
|
94
|
+
{ dataSourceKey: 'main', resourceNames: ['posts'] },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('still marks dirty for direct runAction writes', async () => {
|
|
99
|
+
const engine = new FlowEngine();
|
|
100
|
+
engine.context.defineProperty('api', {
|
|
101
|
+
value: {
|
|
102
|
+
auth: { locale: 'zh-CN' },
|
|
103
|
+
request: vi.fn(async () => ({ data: { data: { id: 1 }, meta: {} } })),
|
|
104
|
+
resource: vi.fn(),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const multi = engine.createResource(MultiRecordResource);
|
|
109
|
+
multi.setDataSourceKey('main').setResourceName('posts');
|
|
110
|
+
await multi.runAction('create', { data: { title: 't' } });
|
|
111
|
+
|
|
112
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
113
|
+
});
|
|
63
114
|
});
|
|
@@ -104,4 +104,32 @@ describe('FlowModelContext.openView - navigation enforcement', () => {
|
|
|
104
104
|
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
105
105
|
expect(child.dispatchEvent.mock.calls[0][0]).toBe('click');
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
it('inherits current model input args when opening an external popup', async () => {
|
|
109
|
+
const { parent, child } = setup();
|
|
110
|
+
parent['getInputArgs'] = vi.fn(() => ({
|
|
111
|
+
filterByTk: 2,
|
|
112
|
+
sourceId: 10,
|
|
113
|
+
defaultInputKeys: ['filterByTk', 'sourceId'],
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
await (parent.context as any).openView('child-uid', { mode: 'dialog' });
|
|
117
|
+
|
|
118
|
+
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(child.dispatchEvent.mock.calls[0][1]).toMatchObject({
|
|
120
|
+
mode: 'dialog',
|
|
121
|
+
filterByTk: 2,
|
|
122
|
+
sourceId: 10,
|
|
123
|
+
});
|
|
124
|
+
expect(child.dispatchEvent.mock.calls[0][1]).not.toHaveProperty('defaultInputKeys');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does not debounce external popup dispatches', async () => {
|
|
128
|
+
const { parent, child } = setup();
|
|
129
|
+
|
|
130
|
+
await (parent.context as any).openView('child-uid', { mode: 'dialog', filterByTk: 1 });
|
|
131
|
+
|
|
132
|
+
expect(child.dispatchEvent).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(child.dispatchEvent.mock.calls[0][2]).toBeUndefined();
|
|
134
|
+
});
|
|
107
135
|
});
|
|
@@ -557,6 +557,78 @@ describe('FlowSettings', () => {
|
|
|
557
557
|
});
|
|
558
558
|
});
|
|
559
559
|
|
|
560
|
+
describe('Dynamic Flow Source Providers', () => {
|
|
561
|
+
test('should return current model as the default dynamic flow source', async () => {
|
|
562
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
563
|
+
|
|
564
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
565
|
+
|
|
566
|
+
expect(sources).toHaveLength(1);
|
|
567
|
+
expect(sources[0].key).toBe('self');
|
|
568
|
+
expect(sources[0].label).toBe('Current block');
|
|
569
|
+
expect(sources[0].model).toBe(model);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('should register, resolve, and dispose dynamic flow source providers', async () => {
|
|
573
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
574
|
+
const target = new FlowModel({ uid: 'target-model', flowEngine: engine });
|
|
575
|
+
|
|
576
|
+
const dispose = flowSettings.registerDynamicFlowSourceProvider({
|
|
577
|
+
key: 'test-provider',
|
|
578
|
+
visible: (m) => m.uid === model.uid,
|
|
579
|
+
getSources: () => [{ key: 'target', label: 'Target model', model: target, sort: 10 }],
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(flowSettings.hasDynamicFlowSourceProvider(model)).toBe(true);
|
|
583
|
+
expect(flowSettings.hasDynamicFlowSourceProvider(target)).toBe(false);
|
|
584
|
+
|
|
585
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
586
|
+
expect(sources.map((source) => source.key)).toEqual(['self', 'target']);
|
|
587
|
+
|
|
588
|
+
dispose();
|
|
589
|
+
|
|
590
|
+
expect(flowSettings.hasDynamicFlowSourceProvider(model)).toBe(false);
|
|
591
|
+
await expect(flowSettings.getDynamicFlowSources(model)).resolves.toHaveLength(1);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('should skip duplicate dynamic flow source keys and model uids', async () => {
|
|
595
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
596
|
+
const target = new FlowModel({ uid: 'target-model', flowEngine: engine });
|
|
597
|
+
|
|
598
|
+
flowSettings.registerDynamicFlowSourceProvider({
|
|
599
|
+
key: 'test-provider',
|
|
600
|
+
getSources: () => [
|
|
601
|
+
{ key: 'target', label: 'Target model', model: target },
|
|
602
|
+
{ key: 'target', label: 'Duplicate key', model: new FlowModel({ uid: 'other-model', flowEngine: engine }) },
|
|
603
|
+
{ key: 'duplicate-uid', label: 'Duplicate uid', model: target },
|
|
604
|
+
],
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
608
|
+
|
|
609
|
+
expect(sources.map((source) => source.key)).toEqual(['self', 'target']);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('should ignore failing dynamic flow source providers', async () => {
|
|
613
|
+
const model = new FlowModel({ uid: 'source-model', flowEngine: engine });
|
|
614
|
+
|
|
615
|
+
flowSettings.registerDynamicFlowSourceProvider({
|
|
616
|
+
key: 'failing-provider',
|
|
617
|
+
getSources: () => {
|
|
618
|
+
throw new Error('provider failed');
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const sources = await flowSettings.getDynamicFlowSources(model);
|
|
623
|
+
|
|
624
|
+
expect(sources.map((source) => source.key)).toEqual(['self']);
|
|
625
|
+
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
626
|
+
"FlowSettings: Dynamic flow source provider 'failing-provider' failed.",
|
|
627
|
+
expect.any(Error),
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
560
632
|
describe('Step Settings Dialog', () => {
|
|
561
633
|
test('should call openStepSettingsDialog with correct parameters', async () => {
|
|
562
634
|
const { openStepSettingsDialog } = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
|
11
|
-
import { FlowEngineContext, FlowRunJSContext } from '../flowContext';
|
|
11
|
+
import { FlowContext, FlowEngineContext, FlowRunJSContext } from '../flowContext';
|
|
12
|
+
import { RUNJS_OPEN_VIEW_ROUTE_STATE } from '../utils/openViewRouteState';
|
|
12
13
|
import { FlowEngine } from '../flowEngine';
|
|
13
|
-
import { FlowContext } from '../flowContext';
|
|
14
14
|
import { setupRunJSContexts } from '../runjs-context/setup';
|
|
15
15
|
import { createJSRunnerWithVersion } from '..';
|
|
16
16
|
import { RunJSContextRegistry } from '../runjs-context/registry';
|
|
@@ -267,6 +267,19 @@ describe('RunJS Runtime Features', () => {
|
|
|
267
267
|
expect(runCtx.libs.dayjs).toBeDefined();
|
|
268
268
|
expect(runCtx.libs.antdIcons).toBeDefined();
|
|
269
269
|
});
|
|
270
|
+
|
|
271
|
+
it('should mark ctx.openView calls with route state only when RunJS passes display overrides', async () => {
|
|
272
|
+
const parentCtx = new FlowContext();
|
|
273
|
+
const openView = vi.fn(async () => undefined);
|
|
274
|
+
parentCtx.defineMethod('openView', openView);
|
|
275
|
+
|
|
276
|
+
const runCtx = new FlowRunJSContext(parentCtx);
|
|
277
|
+
await (runCtx as any).openView('popup', { mode: 'dialog', size: 'large' });
|
|
278
|
+
await (runCtx as any).openView('popup', { filterByTk: 1 });
|
|
279
|
+
|
|
280
|
+
expect(openView.mock.calls[0][1][RUNJS_OPEN_VIEW_ROUTE_STATE]).toEqual({ mode: 'dialog', size: 'large' });
|
|
281
|
+
expect(Object.prototype.hasOwnProperty.call(openView.mock.calls[1][1], RUNJS_OPEN_VIEW_ROUTE_STATE)).toBe(false);
|
|
282
|
+
});
|
|
270
283
|
});
|
|
271
284
|
|
|
272
285
|
describe('Actual code execution', () => {
|
|
@@ -340,9 +340,9 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
340
340
|
const repository = new DirtyPageRepository();
|
|
341
341
|
root.setModelRepository(repository);
|
|
342
342
|
|
|
343
|
-
class ParentModel extends FlowModel {}
|
|
344
|
-
class PageModel extends FlowModel {}
|
|
345
343
|
class BlockModel extends FlowModel {}
|
|
344
|
+
class PageModel extends FlowModel<{ parent?: FlowModel; subModels: { items: BlockModel[] } }> {}
|
|
345
|
+
class ParentModel extends FlowModel<{ parent?: FlowModel; subModels: { page?: PageModel } }> {}
|
|
346
346
|
root.registerModels({ ParentModel, PageModel, BlockModel });
|
|
347
347
|
|
|
348
348
|
const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'popup-action' });
|
|
@@ -357,7 +357,10 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
357
357
|
items: [{ use: 'BlockModel', uid: 'stale-block' }],
|
|
358
358
|
},
|
|
359
359
|
});
|
|
360
|
-
const staleBlock = stalePage.findSubModel('items'
|
|
360
|
+
const staleBlock = stalePage.findSubModel('items', (item) => item.uid === 'stale-block');
|
|
361
|
+
if (!staleBlock) {
|
|
362
|
+
throw new Error('Expected stale block to be loaded');
|
|
363
|
+
}
|
|
361
364
|
parent.setSubModel('page', stalePage);
|
|
362
365
|
oldScoped.unlinkFromStack();
|
|
363
366
|
|
|
@@ -372,7 +375,7 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
372
375
|
},
|
|
373
376
|
};
|
|
374
377
|
|
|
375
|
-
root.flowSettings.enable();
|
|
378
|
+
await root.flowSettings.enable();
|
|
376
379
|
await staleBlock.saveStepParams();
|
|
377
380
|
root.flowSettings.disable();
|
|
378
381
|
repository.findOneCalls = 0;
|
|
@@ -388,8 +391,8 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
388
391
|
|
|
389
392
|
expect(repository.findOneCalls).toBe(1);
|
|
390
393
|
expect(loaded).not.toBe(stalePage);
|
|
391
|
-
expect(
|
|
392
|
-
expect(loaded?.mapSubModels('items'
|
|
394
|
+
expect(parent.subModels.page).toBe(loaded);
|
|
395
|
+
expect(loaded?.mapSubModels('items', (item) => item.uid)).toEqual(['fresh-block']);
|
|
393
396
|
|
|
394
397
|
repository.findOneCalls = 0;
|
|
395
398
|
const nextRuntimeScoped = createViewScopedEngine(root);
|
|
@@ -405,6 +408,69 @@ describe('ViewScopedFlowEngine', () => {
|
|
|
405
408
|
expect(loadedAgain?.uid).toBe('popup-page');
|
|
406
409
|
});
|
|
407
410
|
|
|
411
|
+
it('reloads a page after it was loaded in flow settings mode', async () => {
|
|
412
|
+
const root = new FlowEngine();
|
|
413
|
+
const repository = new DirtyPageRepository();
|
|
414
|
+
root.setModelRepository(repository);
|
|
415
|
+
|
|
416
|
+
class ParentModel extends FlowModel {}
|
|
417
|
+
class PageModel extends FlowModel {}
|
|
418
|
+
class BlockModel extends FlowModel {}
|
|
419
|
+
root.registerModels({ ParentModel, PageModel, BlockModel });
|
|
420
|
+
|
|
421
|
+
const parent = root.createModel<ParentModel>({ use: 'ParentModel', uid: 'settings-popup-action' });
|
|
422
|
+
repository.data = {
|
|
423
|
+
use: 'PageModel',
|
|
424
|
+
uid: 'settings-popup-page',
|
|
425
|
+
parentId: parent.uid,
|
|
426
|
+
subKey: 'page',
|
|
427
|
+
subType: 'object',
|
|
428
|
+
subModels: {
|
|
429
|
+
items: [{ use: 'BlockModel', uid: 'stale-settings-block' }],
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
await root.flowSettings.enable();
|
|
434
|
+
const designScoped = createViewScopedEngine(root);
|
|
435
|
+
const designLoaded = await designScoped.loadOrCreateModel<PageModel>({
|
|
436
|
+
async: true,
|
|
437
|
+
parentId: parent.uid,
|
|
438
|
+
subKey: 'page',
|
|
439
|
+
subType: 'object',
|
|
440
|
+
use: 'PageModel',
|
|
441
|
+
});
|
|
442
|
+
expect(repository.findOneCalls).toBe(1);
|
|
443
|
+
expect(designLoaded?.mapSubModels('items', (item) => item.uid)).toEqual(['stale-settings-block']);
|
|
444
|
+
designScoped.unlinkFromStack();
|
|
445
|
+
|
|
446
|
+
repository.data = {
|
|
447
|
+
use: 'PageModel',
|
|
448
|
+
uid: 'settings-popup-page',
|
|
449
|
+
parentId: parent.uid,
|
|
450
|
+
subKey: 'page',
|
|
451
|
+
subType: 'object',
|
|
452
|
+
subModels: {
|
|
453
|
+
items: [{ use: 'BlockModel', uid: 'fresh-settings-block' }],
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
root.flowSettings.disable();
|
|
457
|
+
repository.findOneCalls = 0;
|
|
458
|
+
|
|
459
|
+
const runtimeScoped = createViewScopedEngine(root);
|
|
460
|
+
const runtimeLoaded = await runtimeScoped.loadOrCreateModel<PageModel>({
|
|
461
|
+
async: true,
|
|
462
|
+
parentId: parent.uid,
|
|
463
|
+
subKey: 'page',
|
|
464
|
+
subType: 'object',
|
|
465
|
+
use: 'PageModel',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(repository.findOneCalls).toBe(1);
|
|
469
|
+
expect(runtimeLoaded).not.toBe(designLoaded);
|
|
470
|
+
expect(parent.subModels.page).toBe(runtimeLoaded);
|
|
471
|
+
expect(runtimeLoaded?.mapSubModels('items', (item) => item.uid)).toEqual(['fresh-settings-block']);
|
|
472
|
+
});
|
|
473
|
+
|
|
408
474
|
it('does not bypass loaded page cache after a non-config save', async () => {
|
|
409
475
|
const root = new FlowEngine();
|
|
410
476
|
const repository = new DirtyPageRepository();
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { FlowModelRendererProps } from './FlowModelRenderer';
|
|
11
11
|
import { FlowModelRenderer } from './FlowModelRenderer';
|
|
12
12
|
import _ from 'lodash';
|
|
13
|
-
import React, { useEffect, useMemo, useRef } from 'react';
|
|
13
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
14
14
|
|
|
15
15
|
const flowModelRendererPropKeys: (keyof FlowModelRendererProps)[] = [
|
|
16
16
|
'model',
|
|
@@ -28,49 +28,63 @@ const flowModelRendererPropKeys: (keyof FlowModelRendererProps)[] = [
|
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
export function FieldModelRenderer(props: any) {
|
|
31
|
-
const { model, ...rest } = props;
|
|
31
|
+
const { model, onChange, ...rest } = props;
|
|
32
32
|
const composingRef = useRef(false);
|
|
33
33
|
|
|
34
|
-
const handleChange = (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
const handleChange = useCallback(
|
|
35
|
+
(e: any) => {
|
|
36
|
+
let val;
|
|
37
|
+
if (e && e.target && typeof e.target.value !== 'undefined') {
|
|
38
|
+
val = e.target.value;
|
|
39
|
+
} else if (
|
|
40
|
+
typeof e === 'string' ||
|
|
41
|
+
typeof e === 'number' ||
|
|
42
|
+
typeof e === 'boolean' ||
|
|
43
|
+
(typeof e === 'object' && !(e instanceof Event))
|
|
44
|
+
) {
|
|
45
|
+
val = e;
|
|
46
|
+
} else {
|
|
47
|
+
val = null;
|
|
48
|
+
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const isComposing = composingRef.current || e?.nativeEvent?.isComposing;
|
|
51
|
+
if (isComposing) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
model.setProps({ value: val });
|
|
56
|
+
if (!composingRef.current) {
|
|
57
|
+
onChange?.(val);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[model, onChange],
|
|
61
|
+
);
|
|
62
|
+
const handleCompositionStart = useCallback(() => {
|
|
55
63
|
composingRef.current = true;
|
|
56
|
-
};
|
|
64
|
+
}, []);
|
|
57
65
|
|
|
58
|
-
const handleCompositionEnd = (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const handleCompositionEnd = useCallback(
|
|
67
|
+
(e: React.CompositionEvent<HTMLInputElement>, flag = true) => {
|
|
68
|
+
composingRef.current = false;
|
|
69
|
+
if (flag) {
|
|
70
|
+
handleChange(e);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[handleChange],
|
|
74
|
+
);
|
|
64
75
|
|
|
65
|
-
const modelProps =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
const modelProps = useMemo(
|
|
77
|
+
() => ({
|
|
78
|
+
onCompositionStart: handleCompositionStart,
|
|
79
|
+
onCompositionEnd: handleCompositionEnd,
|
|
80
|
+
..._.omit(rest, flowModelRendererPropKeys),
|
|
81
|
+
onChange: handleChange,
|
|
82
|
+
}),
|
|
83
|
+
[handleChange, handleCompositionEnd, handleCompositionStart, rest],
|
|
84
|
+
);
|
|
71
85
|
useEffect(() => {
|
|
72
86
|
model && model.setProps(modelProps);
|
|
73
|
-
}, [modelProps]);
|
|
87
|
+
}, [model, modelProps]);
|
|
74
88
|
|
|
75
89
|
return <FlowModelRenderer model={model} {...rest} />;
|
|
76
90
|
}
|
|
@@ -11,7 +11,7 @@ import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
|
|
|
11
11
|
import { useStyleRegister } from '@ant-design/cssinjs';
|
|
12
12
|
import { merge } from '@formily/shared';
|
|
13
13
|
import type { ComponentTokenMap } from 'antd/es/theme/interface';
|
|
14
|
-
import {
|
|
14
|
+
import { useContext, useRef } from 'react';
|
|
15
15
|
import { ConfigProvider, theme } from 'antd';
|
|
16
16
|
|
|
17
17
|
const usePrefixCls = (
|
|
@@ -83,6 +83,10 @@ type UseComponentStyleResult = {
|
|
|
83
83
|
componentCls: string;
|
|
84
84
|
rootPrefixCls: string;
|
|
85
85
|
};
|
|
86
|
+
type WrapSSRCache = {
|
|
87
|
+
deps: unknown[];
|
|
88
|
+
wrapSSR: ReturnType<typeof useStyleRegister>;
|
|
89
|
+
};
|
|
86
90
|
|
|
87
91
|
const genStyleHook = <ComponentName extends OverrideComponent>(
|
|
88
92
|
component: ComponentName,
|
|
@@ -122,9 +126,18 @@ const genStyleHook = <ComponentName extends OverrideComponent>(
|
|
|
122
126
|
|
|
123
127
|
// useStyleRegister 有 BUG,会导致重复渲染,所以这里做了一层缓存
|
|
124
128
|
// 等 https://github.com/ant-design/cssinjs/pull/176 合并后,可以去掉这层缓存
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
const wrapSSRDeps = [theme, token, hashId, prefixCls, iconPrefixCls, rootPrefixCls, props];
|
|
130
|
+
const wrapSSRCacheRef = useRef<WrapSSRCache>();
|
|
131
|
+
const currentCache = wrapSSRCacheRef.current;
|
|
132
|
+
let memoizedWrapSSR: ReturnType<typeof useStyleRegister> = currentCache?.wrapSSR || wrapSSR;
|
|
133
|
+
|
|
134
|
+
if (!currentCache || wrapSSRDeps.some((dep, index) => dep !== currentCache.deps[index])) {
|
|
135
|
+
wrapSSRCacheRef.current = {
|
|
136
|
+
deps: wrapSSRDeps,
|
|
137
|
+
wrapSSR,
|
|
138
|
+
};
|
|
139
|
+
memoizedWrapSSR = wrapSSR;
|
|
140
|
+
}
|
|
128
141
|
|
|
129
142
|
return {
|
|
130
143
|
wrapSSR: memoizedWrapSSR,
|
|
@@ -140,14 +153,14 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
140
153
|
return {
|
|
141
154
|
[componentCls]: {
|
|
142
155
|
'.nb-mobile-action-drawer-header': {
|
|
143
|
-
height: 'var(--nb-mobile-page-header-height)',
|
|
156
|
+
height: 'var(--nb-mobile-page-header-height, 46px)',
|
|
144
157
|
display: 'flex',
|
|
145
158
|
alignItems: 'center',
|
|
146
159
|
justifyContent: 'space-between',
|
|
147
160
|
borderBottom: `1px solid ${token.colorSplit}`,
|
|
148
161
|
position: 'sticky',
|
|
149
162
|
top: 0,
|
|
150
|
-
backgroundColor:
|
|
163
|
+
backgroundColor: token.colorBgContainer,
|
|
151
164
|
zIndex: 1000,
|
|
152
165
|
|
|
153
166
|
// to match the button named 'Add block'
|
|
@@ -159,19 +172,33 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
159
172
|
'.nb-mobile-action-drawer-placeholder': {
|
|
160
173
|
display: 'inline-block',
|
|
161
174
|
padding: 12,
|
|
175
|
+
flex: '0 0 auto',
|
|
162
176
|
visibility: 'hidden',
|
|
163
177
|
},
|
|
164
178
|
|
|
179
|
+
'.nb-mobile-action-drawer-title': {
|
|
180
|
+
flex: '1 1 auto',
|
|
181
|
+
minWidth: 0,
|
|
182
|
+
overflow: 'hidden',
|
|
183
|
+
textAlign: 'center',
|
|
184
|
+
textOverflow: 'ellipsis',
|
|
185
|
+
whiteSpace: 'nowrap',
|
|
186
|
+
},
|
|
187
|
+
|
|
165
188
|
'.nb-mobile-action-drawer-close-icon': {
|
|
166
189
|
display: 'inline-block',
|
|
167
190
|
padding: 12,
|
|
191
|
+
flex: '0 0 auto',
|
|
168
192
|
cursor: 'pointer',
|
|
169
193
|
},
|
|
170
194
|
|
|
171
195
|
'.nb-mobile-action-drawer-body': {
|
|
172
196
|
borderTopLeftRadius: 8,
|
|
173
197
|
borderTopRightRadius: 8,
|
|
174
|
-
maxHeight: 'calc(
|
|
198
|
+
maxHeight: 'calc(100vh - var(--nb-mobile-page-header-height, 46px))',
|
|
199
|
+
'@supports (height: 100dvh)': {
|
|
200
|
+
maxHeight: 'calc(100dvh - var(--nb-mobile-page-header-height, 46px))',
|
|
201
|
+
},
|
|
175
202
|
overflowY: 'auto',
|
|
176
203
|
overflowX: 'hidden',
|
|
177
204
|
backgroundColor: token.colorBgLayout,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ConfigProvider } from 'antd';
|
|
11
|
-
import React, { FC, ReactNode, useMemo } from 'react';
|
|
11
|
+
import React, { FC, ReactNode, useCallback, useMemo } from 'react';
|
|
12
12
|
import { useMobileActionDrawerStyle } from './MobilePopup.style';
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
14
|
import { lazy } from '../lazy-helper';
|
|
@@ -31,11 +31,33 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
|
|
|
31
31
|
const { t } = useTranslation();
|
|
32
32
|
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const bodyStyles = (props as MobilePopupProps & { styles?: { body?: React.CSSProperties } }).styles?.body;
|
|
35
|
+
const popupStyle = useMemo(() => {
|
|
35
36
|
return {
|
|
36
|
-
minHeight,
|
|
37
|
+
minHeight: bodyStyles?.minHeight ?? minHeight,
|
|
38
|
+
height: bodyStyles?.height,
|
|
39
|
+
maxHeight: bodyStyles?.maxHeight,
|
|
37
40
|
};
|
|
38
|
-
}, [minHeight]);
|
|
41
|
+
}, [bodyStyles?.height, bodyStyles?.maxHeight, bodyStyles?.minHeight, minHeight]);
|
|
42
|
+
|
|
43
|
+
const bodyStyle = useMemo(() => {
|
|
44
|
+
return {
|
|
45
|
+
padding: 0,
|
|
46
|
+
...bodyStyles,
|
|
47
|
+
};
|
|
48
|
+
}, [bodyStyles]);
|
|
49
|
+
|
|
50
|
+
const handleCloseKeyDown = useCallback(
|
|
51
|
+
(event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
52
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
closePopup();
|
|
58
|
+
},
|
|
59
|
+
[closePopup],
|
|
60
|
+
);
|
|
39
61
|
|
|
40
62
|
const theme = useMemo(() => {
|
|
41
63
|
return {
|
|
@@ -57,11 +79,8 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
|
|
|
57
79
|
onClose={closePopup}
|
|
58
80
|
onMaskClick={closePopup}
|
|
59
81
|
bodyClassName="nb-mobile-action-drawer-body"
|
|
60
|
-
bodyStyle={
|
|
61
|
-
|
|
62
|
-
}}
|
|
63
|
-
maskStyle={style}
|
|
64
|
-
style={style}
|
|
82
|
+
bodyStyle={bodyStyle}
|
|
83
|
+
style={popupStyle}
|
|
65
84
|
destroyOnClose
|
|
66
85
|
>
|
|
67
86
|
<div className="nb-mobile-action-drawer-header">
|
|
@@ -69,13 +88,14 @@ export const MobilePopup: FC<MobilePopupProps> = (props) => {
|
|
|
69
88
|
<span className="nb-mobile-action-drawer-placeholder">
|
|
70
89
|
<CloseOutline />
|
|
71
90
|
</span>
|
|
72
|
-
<span>{title}</span>
|
|
91
|
+
<span className="nb-mobile-action-drawer-title">{title}</span>
|
|
73
92
|
<span
|
|
74
93
|
className="nb-mobile-action-drawer-close-icon"
|
|
75
94
|
onClick={closePopup}
|
|
76
95
|
role="button"
|
|
77
96
|
tabIndex={0}
|
|
78
97
|
aria-label={t('Close')}
|
|
98
|
+
onKeyDown={handleCloseKeyDown}
|
|
79
99
|
>
|
|
80
100
|
<CloseOutline />
|
|
81
101
|
</span>
|