@nocobase/flow-engine 2.2.0-beta.7 → 2.2.0-beta.9
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/components/MobilePopup.style.js +16 -5
- package/lib/flowContext.d.ts +1 -1
- package/lib/flowContext.js +32 -12
- 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/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 -0
- package/lib/utils/index.js +11 -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/views/ViewNavigation.js +5 -0
- package/package.json +4 -4
- package/src/__tests__/flowContext.test.ts +111 -0
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
- package/src/components/MobilePopup.style.ts +22 -6
- package/src/components/__tests__/MobilePopup.style.test.tsx +103 -0
- package/src/flowContext.ts +36 -10
- 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/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 -0
- package/src/utils/openViewRouteState.ts +107 -0
- package/src/utils/parsePathnameToViewParams.ts +23 -1
- package/src/views/ViewNavigation.ts +6 -1
- package/src/views/__tests__/ViewNavigation.test.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.2.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.2.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.2.0-beta.9",
|
|
12
|
+
"@nocobase/shared": "2.2.0-beta.9",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "60e3d7abbaa0c7cead76f71a4f3d5eedb6b8acdb"
|
|
41
41
|
}
|
|
@@ -14,6 +14,8 @@ import { FlowEngine } from '../flowEngine';
|
|
|
14
14
|
import { FlowModel } from '../models/flowModel';
|
|
15
15
|
import { RunJSContextRegistry } from '../runjs-context/registry';
|
|
16
16
|
import { setupRunJSContexts } from '../runjs-context/setup';
|
|
17
|
+
import { createViewScopedEngine } from '../ViewScopedFlowEngine';
|
|
18
|
+
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
17
19
|
|
|
18
20
|
describe('FlowContext properties and methods', () => {
|
|
19
21
|
it('should return static property value', () => {
|
|
@@ -1429,6 +1431,92 @@ describe('FlowEngine context', () => {
|
|
|
1429
1431
|
expect(engine.context.appName).toBe('NocoBase');
|
|
1430
1432
|
});
|
|
1431
1433
|
|
|
1434
|
+
it('ctx.api should return a dirty-aware wrapper for static api properties', async () => {
|
|
1435
|
+
const engine = new FlowEngine();
|
|
1436
|
+
const update = vi.fn(async () => ({ data: { data: { id: 1 } } }));
|
|
1437
|
+
const api = {
|
|
1438
|
+
auth: { locale: 'zh-CN' },
|
|
1439
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
1440
|
+
resource: vi.fn(() => ({ update })),
|
|
1441
|
+
};
|
|
1442
|
+
engine.context.defineProperty('api', { value: api });
|
|
1443
|
+
|
|
1444
|
+
await engine.context.api.resource('posts').update({ filterByTk: 1, values: { title: 't' } });
|
|
1445
|
+
|
|
1446
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
1447
|
+
expect(engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1448
|
+
expect(engine.context.api).toBe(engine.context.api);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
it('ctx.api should stay dirty-aware when resolved from a scoped context delegate', async () => {
|
|
1452
|
+
const root = new FlowEngine();
|
|
1453
|
+
const scoped = createViewScopedEngine(root);
|
|
1454
|
+
const update = vi.fn(async () => ({ data: { data: { id: 1 } } }));
|
|
1455
|
+
root.context.defineProperty('api', {
|
|
1456
|
+
value: {
|
|
1457
|
+
auth: { locale: 'zh-CN' },
|
|
1458
|
+
request: vi.fn(async () => ({ data: { ok: true } })),
|
|
1459
|
+
resource: vi.fn(() => ({ update })),
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
await scoped.context.api.resource('posts').update({ filterByTk: 1, values: { title: 't' } });
|
|
1464
|
+
|
|
1465
|
+
expect(update).toHaveBeenCalledTimes(1);
|
|
1466
|
+
expect(root.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1467
|
+
expect(scoped.context.engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it('ctx.request should use the dirty-aware api wrapper', async () => {
|
|
1471
|
+
const engine = new FlowEngine();
|
|
1472
|
+
const request = vi.fn(async () => ({ data: { ok: true } }));
|
|
1473
|
+
engine.context.defineProperty('api', {
|
|
1474
|
+
value: {
|
|
1475
|
+
auth: { locale: 'zh-CN' },
|
|
1476
|
+
request,
|
|
1477
|
+
resource: vi.fn(),
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
await engine.context.request({
|
|
1482
|
+
resource: 'posts',
|
|
1483
|
+
action: 'update',
|
|
1484
|
+
headers: { 'X-Data-Source': 'analytics' },
|
|
1485
|
+
params: { filterByTk: 1 },
|
|
1486
|
+
} as any);
|
|
1487
|
+
|
|
1488
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
1489
|
+
expect(engine.getDataSourceDirtyVersion('analytics', 'posts')).toBe(1);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('ctx.request should use the caller context when resolved through a scoped delegate', async () => {
|
|
1493
|
+
const root = new FlowEngine();
|
|
1494
|
+
const scoped = createViewScopedEngine(root);
|
|
1495
|
+
const callerCtx = new FlowContext();
|
|
1496
|
+
const dirtyEvents: Array<{ dataSourceKey: string; resourceNames: string[] }> = [];
|
|
1497
|
+
const request = vi.fn(async () => ({ data: { ok: true } }));
|
|
1498
|
+
root.context.defineProperty('api', {
|
|
1499
|
+
value: {
|
|
1500
|
+
auth: { locale: 'zh-CN' },
|
|
1501
|
+
request,
|
|
1502
|
+
resource: vi.fn(),
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
callerCtx.addDelegate(scoped.context);
|
|
1506
|
+
scoped.context.engine.emitter.on(DATA_SOURCE_DIRTY_EVENT, (event) => dirtyEvents.push(event));
|
|
1507
|
+
|
|
1508
|
+
await callerCtx.request({
|
|
1509
|
+
resource: 'posts',
|
|
1510
|
+
action: 'update',
|
|
1511
|
+
params: { filterByTk: 1 },
|
|
1512
|
+
} as any);
|
|
1513
|
+
|
|
1514
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
1515
|
+
expect(root.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1516
|
+
expect(scoped.context.engine.getDataSourceDirtyVersion('main', 'posts')).toBe(1);
|
|
1517
|
+
expect(dirtyEvents).toEqual([{ dataSourceKey: 'main', resourceNames: ['posts'] }]);
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1432
1520
|
it('ctx.sql should resolve template variables from caller context in delegate chain', async () => {
|
|
1433
1521
|
const engine = new FlowEngine();
|
|
1434
1522
|
const request = vi.fn(async () => ({ data: { data: [] } }));
|
|
@@ -1519,6 +1607,29 @@ describe('FlowEngine context', () => {
|
|
|
1519
1607
|
await expect((engine.context as any).getVar('foo.bar')).rejects.toThrow();
|
|
1520
1608
|
});
|
|
1521
1609
|
|
|
1610
|
+
it('model.context.getVar should resolve the latest ctx.auth.user after user changes', async () => {
|
|
1611
|
+
const engine = new FlowEngine();
|
|
1612
|
+
engine.context.defineProperty('api', {
|
|
1613
|
+
value: { auth: { role: 'admin', locale: 'en-US', token: 'token-1' } },
|
|
1614
|
+
});
|
|
1615
|
+
engine.context.defineProperty('user', { value: { id: 1, nickname: 'Alice' } });
|
|
1616
|
+
|
|
1617
|
+
class AuthUserModel extends FlowModel {}
|
|
1618
|
+
engine.registerModels({ AuthUserModel });
|
|
1619
|
+
const model = engine.createModel({ use: 'AuthUserModel' });
|
|
1620
|
+
|
|
1621
|
+
expect(await model.context.getVar('ctx.auth.user.id')).toBe(1);
|
|
1622
|
+
expect(await model.context.getVar('ctx.auth.token')).toBe('token-1');
|
|
1623
|
+
|
|
1624
|
+
engine.context.api.auth.role = 'member';
|
|
1625
|
+
engine.context.api.auth.token = 'token-2';
|
|
1626
|
+
engine.context.defineProperty('user', { value: { id: 2, nickname: 'Bob' } });
|
|
1627
|
+
|
|
1628
|
+
expect(await model.context.getVar('ctx.auth.user.id')).toBe(2);
|
|
1629
|
+
expect(await model.context.getVar('ctx.auth.roleName')).toBe('member');
|
|
1630
|
+
expect(await model.context.getVar('ctx.auth.token')).toBe('token-2');
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1522
1633
|
it('engine.context.runAction should resolve action from engine.getAction', async () => {
|
|
1523
1634
|
const engine = new FlowEngine();
|
|
1524
1635
|
|
|
@@ -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
|
});
|
|
@@ -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', () => {
|
|
@@ -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,7 +153,7 @@ 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',
|
|
@@ -171,7 +184,10 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
|
|
171
184
|
'.nb-mobile-action-drawer-body': {
|
|
172
185
|
borderTopLeftRadius: 8,
|
|
173
186
|
borderTopRightRadius: 8,
|
|
174
|
-
maxHeight: 'calc(
|
|
187
|
+
maxHeight: 'calc(100vh - var(--nb-mobile-page-header-height, 46px))',
|
|
188
|
+
'@supports (height: 100dvh)': {
|
|
189
|
+
maxHeight: 'calc(100dvh - var(--nb-mobile-page-header-height, 46px))',
|
|
190
|
+
},
|
|
175
191
|
overflowY: 'auto',
|
|
176
192
|
overflowX: 'hidden',
|
|
177
193
|
backgroundColor: token.colorBgLayout,
|
|
@@ -0,0 +1,103 @@
|
|
|
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 React, { CSSProperties, ReactNode } from 'react';
|
|
11
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import { ConfigProvider } from 'antd';
|
|
13
|
+
import { vi } from 'vitest';
|
|
14
|
+
import { MobilePopup } from '../MobilePopup';
|
|
15
|
+
import { useMobileActionDrawerStyle } from '../MobilePopup.style';
|
|
16
|
+
|
|
17
|
+
vi.mock('antd-mobile', () => ({
|
|
18
|
+
Popup: ({
|
|
19
|
+
bodyClassName,
|
|
20
|
+
bodyStyle,
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
visible,
|
|
24
|
+
}: {
|
|
25
|
+
bodyClassName?: string;
|
|
26
|
+
bodyStyle?: CSSProperties;
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
visible?: boolean;
|
|
30
|
+
}) =>
|
|
31
|
+
visible ? (
|
|
32
|
+
<div className={className} data-testid="popup-root">
|
|
33
|
+
<div className={`adm-popup-body ${bodyClassName || ''}`} data-testid="popup-body" style={bodyStyle}>
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
) : null,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock('antd-mobile-icons', () => ({
|
|
41
|
+
CloseOutline: () => <span data-testid="close-outline" />,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('react-i18next', () => ({
|
|
45
|
+
useTranslation: () => ({
|
|
46
|
+
t: (key: string) => key,
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
function StyleProbe() {
|
|
51
|
+
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={`${componentCls} ${hashId}`}>
|
|
55
|
+
<div className="nb-mobile-action-drawer-header" data-testid="drawer-header" />
|
|
56
|
+
<div className="nb-mobile-action-drawer-body" data-testid="drawer-body" />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getInjectedStyles() {
|
|
62
|
+
return Array.from(document.querySelectorAll('style'))
|
|
63
|
+
.map((style) => style.textContent || '')
|
|
64
|
+
.join('\n')
|
|
65
|
+
.replace(/\s+/g, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('MobilePopup styles', () => {
|
|
69
|
+
it('mounts the action drawer class on the real popup body', async () => {
|
|
70
|
+
render(
|
|
71
|
+
<MobilePopup visible title="Edit" onClose={vi.fn()}>
|
|
72
|
+
<div style={{ height: 1200 }}>Long content</div>
|
|
73
|
+
</MobilePopup>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const popupBody = await screen.findByTestId('popup-body');
|
|
77
|
+
|
|
78
|
+
expect(screen.getByTestId('popup-root')).toHaveClass('ant-nb-mobile-action-drawer');
|
|
79
|
+
expect(popupBody).toHaveClass('adm-popup-body');
|
|
80
|
+
expect(popupBody).toHaveClass('nb-mobile-action-drawer-body');
|
|
81
|
+
expect(popupBody).toHaveStyle({ padding: '0px' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('limits the mobile action drawer body by viewport height', async () => {
|
|
85
|
+
render(
|
|
86
|
+
<ConfigProvider>
|
|
87
|
+
<StyleProbe />
|
|
88
|
+
</ConfigProvider>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(getInjectedStyles()).toContain('.nb-mobile-action-drawer-body');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const styles = getInjectedStyles();
|
|
96
|
+
|
|
97
|
+
expect(styles).toContain('height:var(--nb-mobile-page-header-height,46px)');
|
|
98
|
+
expect(styles).toContain('max-height:calc(100vh-var(--nb-mobile-page-header-height,46px))');
|
|
99
|
+
expect(styles).toContain('max-height:calc(100dvh-var(--nb-mobile-page-header-height,46px))');
|
|
100
|
+
expect(styles).toContain('overflow-y:auto');
|
|
101
|
+
expect(styles).not.toContain('max-height:calc(100%-var(--nb-mobile-page-header-height))');
|
|
102
|
+
});
|
|
103
|
+
});
|
package/src/flowContext.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { ISchema } from '@formily/json-schema';
|
|
11
11
|
import { observable } from '@formily/reactive';
|
|
12
|
-
import { APIClient, RequestOptions } from '@nocobase/sdk';
|
|
12
|
+
import type { APIClient, RequestOptions } from '@nocobase/sdk';
|
|
13
13
|
import type { Router } from '@remix-run/router';
|
|
14
14
|
import axios from 'axios';
|
|
15
15
|
import { MessageInstance } from 'antd/es/message/interface';
|
|
@@ -39,9 +39,11 @@ import {
|
|
|
39
39
|
extractUsedVariablePaths,
|
|
40
40
|
FlowExitException,
|
|
41
41
|
FLOW_ENGINE_NAMESPACE,
|
|
42
|
+
createOpenViewRouteState,
|
|
42
43
|
isCtxDatePathPrefix,
|
|
43
44
|
isCssFile,
|
|
44
45
|
prepareRunJsCode,
|
|
46
|
+
RUNJS_OPEN_VIEW_ROUTE_STATE,
|
|
45
47
|
resolveCtxDatePath,
|
|
46
48
|
resolveDefaultParams,
|
|
47
49
|
resolveExpressions,
|
|
@@ -51,6 +53,7 @@ import { FlowExitAllException } from './utils/exceptions';
|
|
|
51
53
|
import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
|
|
52
54
|
import type { RecordRef } from './utils/serverContextParams';
|
|
53
55
|
import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
|
|
56
|
+
import { getDirtyAwareApiClient } from './utils/dirtyAwareApiClient';
|
|
54
57
|
import { inferRecordRef } from './utils/variablesParams';
|
|
55
58
|
import { FlowView, FlowViewer } from './views/FlowView';
|
|
56
59
|
import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './runjs-context/registry';
|
|
@@ -2909,19 +2912,20 @@ export class FlowContext {
|
|
|
2909
2912
|
|
|
2910
2913
|
// 静态值
|
|
2911
2914
|
if ('value' in options) {
|
|
2912
|
-
return options.value;
|
|
2915
|
+
return key === 'api' ? getDirtyAwareApiClient(options.value, currentContext) : options.value;
|
|
2913
2916
|
}
|
|
2914
2917
|
|
|
2915
2918
|
// get 方法
|
|
2916
2919
|
if (options.get) {
|
|
2917
2920
|
if (options.cache === false) {
|
|
2918
|
-
|
|
2921
|
+
const value = options.get(currentContext);
|
|
2922
|
+
return key === 'api' ? getDirtyAwareApiClient(value, currentContext) : value;
|
|
2919
2923
|
}
|
|
2920
2924
|
|
|
2921
2925
|
const cacheKey = options.observable ? '_observableCache' : '_cache';
|
|
2922
2926
|
|
|
2923
2927
|
if (key in this[cacheKey]) {
|
|
2924
|
-
return this[cacheKey][key];
|
|
2928
|
+
return key === 'api' ? getDirtyAwareApiClient(this[cacheKey][key], currentContext) : this[cacheKey][key];
|
|
2925
2929
|
}
|
|
2926
2930
|
|
|
2927
2931
|
if (this._pending[key]) return this._pending[key];
|
|
@@ -2939,7 +2943,7 @@ export class FlowContext {
|
|
|
2939
2943
|
(v) => {
|
|
2940
2944
|
this[cacheKey][key] = v;
|
|
2941
2945
|
delete this._pending[key];
|
|
2942
|
-
return v;
|
|
2946
|
+
return key === 'api' ? getDirtyAwareApiClient(v, currentContext) : v;
|
|
2943
2947
|
},
|
|
2944
2948
|
(err) => {
|
|
2945
2949
|
delete this._pending[key];
|
|
@@ -2951,7 +2955,7 @@ export class FlowContext {
|
|
|
2951
2955
|
|
|
2952
2956
|
// sync 直接缓存
|
|
2953
2957
|
this[cacheKey][key] = result;
|
|
2954
|
-
return result;
|
|
2958
|
+
return key === 'api' ? getDirtyAwareApiClient(result, currentContext) : result;
|
|
2955
2959
|
}
|
|
2956
2960
|
|
|
2957
2961
|
return undefined;
|
|
@@ -3074,7 +3078,7 @@ class BaseFlowEngineContext extends FlowContext {
|
|
|
3074
3078
|
this.defineMethod('getModel', (modelName: string, searchInPreviousEngines?: boolean) => {
|
|
3075
3079
|
return this.engine.getModel(modelName, searchInPreviousEngines);
|
|
3076
3080
|
});
|
|
3077
|
-
this.defineMethod('request', (options: RequestOptions)
|
|
3081
|
+
this.defineMethod('request', function (this: FlowContext, options: RequestOptions) {
|
|
3078
3082
|
const app = this.app as { getApiUrl?: (pathname?: string) => string } | undefined;
|
|
3079
3083
|
if (typeof options?.url === 'string' && shouldBypassApiClient(options.url, app)) {
|
|
3080
3084
|
return axios.request(options);
|
|
@@ -3500,11 +3504,12 @@ export class FlowEngineContext extends BaseFlowEngineContext {
|
|
|
3500
3504
|
});
|
|
3501
3505
|
this.defineProperty('auth', {
|
|
3502
3506
|
get: () => ({
|
|
3503
|
-
roleName: this.api
|
|
3504
|
-
locale: this.api
|
|
3505
|
-
token: this.api
|
|
3507
|
+
roleName: this.api?.auth?.role,
|
|
3508
|
+
locale: this.api?.auth?.locale,
|
|
3509
|
+
token: this.api?.auth?.token,
|
|
3506
3510
|
user: this.user,
|
|
3507
3511
|
}),
|
|
3512
|
+
cache: false,
|
|
3508
3513
|
});
|
|
3509
3514
|
this.defineProperty('date', {
|
|
3510
3515
|
get: () => {
|
|
@@ -4600,6 +4605,27 @@ export class FlowRunJSContext extends FlowContext {
|
|
|
4600
4605
|
this.defineProperty('ReactDOM', { value: ReactDOMShim });
|
|
4601
4606
|
|
|
4602
4607
|
setupRunJSLibs(this);
|
|
4608
|
+
this.defineMethod('openView', async function (uid: string, options?: Record<PropertyKey, unknown>) {
|
|
4609
|
+
const delegateOpenView = (
|
|
4610
|
+
delegate as FlowContext & {
|
|
4611
|
+
openView?: (uid: string, options?: Record<PropertyKey, unknown>) => Promise<unknown>;
|
|
4612
|
+
}
|
|
4613
|
+
).openView;
|
|
4614
|
+
|
|
4615
|
+
if (typeof delegateOpenView !== 'function') {
|
|
4616
|
+
throw new Error('ctx.openView is not available in current context.');
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
const routeState = createOpenViewRouteState(options);
|
|
4620
|
+
if (!routeState) {
|
|
4621
|
+
return delegateOpenView(uid, options);
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
return delegateOpenView(uid, {
|
|
4625
|
+
...(options || {}),
|
|
4626
|
+
[RUNJS_OPEN_VIEW_ROUTE_STATE]: routeState,
|
|
4627
|
+
});
|
|
4628
|
+
});
|
|
4603
4629
|
|
|
4604
4630
|
// Convenience: ctx.render(<App />[, container])
|
|
4605
4631
|
// - container defaults to ctx.element if available
|
package/src/locale/en-US.json
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"Replace current block with template?": "Replace current block with template?",
|
|
57
57
|
"Replaced with template block": "Replaced with template block",
|
|
58
58
|
"Render failed": "Render failed",
|
|
59
|
+
"Response record": "Response record",
|
|
59
60
|
"Step configuration": "Step configuration",
|
|
60
61
|
"Step parameter configuration": "Step parameter configuration",
|
|
61
62
|
"Step with key {{stepKey}} not found": "Step with key {{stepKey}} not found",
|
package/src/locale/zh-CN.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { APIClient } from '@nocobase/sdk';
|
|
11
11
|
import { FlowContext } from '../flowContext';
|
|
12
|
+
import { getDirtyAwareApiClient } from '../utils/dirtyAwareApiClient';
|
|
12
13
|
import { FlowResource, ResourceError } from './flowResource';
|
|
13
14
|
|
|
14
15
|
export class APIResource<TData = any> extends FlowResource<TData> {
|
|
@@ -33,7 +34,7 @@ export class APIResource<TData = any> extends FlowResource<TData> {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
setAPIClient(api: APIClient) {
|
|
36
|
-
this.api = api;
|
|
37
|
+
this.api = getDirtyAwareApiClient(api, this.context) as APIClient;
|
|
37
38
|
return this;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -12,7 +12,7 @@ import _ from 'lodash';
|
|
|
12
12
|
import { APIResource } from './apiResource';
|
|
13
13
|
import { FilterItem } from './filterItem';
|
|
14
14
|
import { ResourceError } from './flowResource';
|
|
15
|
-
import {
|
|
15
|
+
import { markDataSourceDirty } from '../utils/dataSourceDirty';
|
|
16
16
|
|
|
17
17
|
export abstract class BaseRecordResource<TData = any> extends APIResource<TData> {
|
|
18
18
|
protected resourceName: string;
|
|
@@ -142,28 +142,11 @@ export abstract class BaseRecordResource<TData = any> extends APIResource<TData>
|
|
|
142
142
|
* Used to coordinate "refresh on active" across view stacks.
|
|
143
143
|
*/
|
|
144
144
|
protected markDataSourceDirty(resourceName?: string) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!resName) return;
|
|
151
|
-
|
|
152
|
-
const affectedResourceNames = new Set<string>([String(resName)]);
|
|
153
|
-
// Optional safety: association resources like "users.profile" may impact parent collection views.
|
|
154
|
-
if (typeof resName === 'string' && resName.includes('.')) {
|
|
155
|
-
affectedResourceNames.add(resName.split('.')[0]);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const name of affectedResourceNames) {
|
|
159
|
-
engine.markDataSourceDirty(dataSourceKey, name);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Signal current view to re-evaluate dirty blocks (e.g., same-view sibling refresh).
|
|
163
|
-
// This is emitted on the *current* engine emitter (view-scoped) so it won't affect other views.
|
|
164
|
-
engine.emitter?.emit?.(DATA_SOURCE_DIRTY_EVENT, {
|
|
165
|
-
dataSourceKey,
|
|
166
|
-
resourceNames: Array.from(affectedResourceNames),
|
|
145
|
+
markDataSourceDirty({
|
|
146
|
+
engine: this.context.engine,
|
|
147
|
+
dataSourceKey: this.getDataSourceKey(),
|
|
148
|
+
resourceName: resourceName || this.getResourceName(),
|
|
149
|
+
includePreviousEngines: true,
|
|
167
150
|
});
|
|
168
151
|
}
|
|
169
152
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { observable } from '@formily/reactive';
|
|
11
11
|
import { AxiosRequestConfig } from 'axios';
|
|
12
12
|
import _ from 'lodash';
|
|
13
|
+
import { SKIP_DATA_SOURCE_DIRTY } from '../utils/dirtyAwareApiClient';
|
|
13
14
|
import { BaseRecordResource } from './baseRecordResource';
|
|
14
15
|
|
|
15
16
|
export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDataItem[]> {
|
|
@@ -113,7 +114,10 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
|
|
|
113
114
|
|
|
114
115
|
async create(data: TDataItem, options?: AxiosRequestConfig & { refresh?: boolean }): Promise<void> {
|
|
115
116
|
const config = this.mergeRequestConfig({ data }, this.createActionOptions, options);
|
|
116
|
-
const res = await this.runAction('create',
|
|
117
|
+
const res = await this.runAction('create', {
|
|
118
|
+
...config,
|
|
119
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
120
|
+
});
|
|
117
121
|
this.markDataSourceDirty();
|
|
118
122
|
this.emit('saved', data);
|
|
119
123
|
if (options?.refresh !== false) {
|
|
@@ -146,7 +150,10 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
|
|
|
146
150
|
this.updateActionOptions,
|
|
147
151
|
options,
|
|
148
152
|
);
|
|
149
|
-
await this.runAction('update',
|
|
153
|
+
await this.runAction('update', {
|
|
154
|
+
...config,
|
|
155
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
156
|
+
});
|
|
150
157
|
this.markDataSourceDirty();
|
|
151
158
|
this.emit('saved', data);
|
|
152
159
|
await this.refresh();
|
|
@@ -172,7 +179,10 @@ export class MultiRecordResource<TDataItem = any> extends BaseRecordResource<TDa
|
|
|
172
179
|
},
|
|
173
180
|
options,
|
|
174
181
|
);
|
|
175
|
-
await this.runAction('destroy',
|
|
182
|
+
await this.runAction('destroy', {
|
|
183
|
+
...config,
|
|
184
|
+
[SKIP_DATA_SOURCE_DIRTY]: true,
|
|
185
|
+
});
|
|
176
186
|
this.markDataSourceDirty();
|
|
177
187
|
const currentPage = this.getPage();
|
|
178
188
|
const lastPage = Math.ceil((this.getCount() - _.castArray(filterByTk).length) / this.getPageSize());
|