@nocobase/client-v2 2.1.26 → 2.1.28
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/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/index.mjs +69 -69
- package/lib/index.js +76 -76
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +8 -7
- package/src/flow/__tests__/FlowRoute.test.tsx +1 -0
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/components/FlowRoute.tsx +6 -28
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
- package/src/flow/models/base/PageModel/PageTabModel.tsx +63 -15
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +489 -6
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/locale/languageCodes.ts +1 -0
|
@@ -64,6 +64,7 @@ const languageCodes = {
|
|
|
64
64
|
"ml-IN": { label: "\u0D2E\u0D32\u0D2F\u0D3E\u0D33\u0D02" },
|
|
65
65
|
"mn-MN": { label: "\u041C\u043E\u043D\u0433\u043E\u043B \u0445\u044D\u043B" },
|
|
66
66
|
"ms-MY": { label: "\u0628\u0647\u0627\u0633 \u0645\u0644\u0627\u064A\u0648" },
|
|
67
|
+
"my-MM": { label: "\u1019\u103C\u1014\u103A\u1019\u102C\u1018\u102C\u101E\u102C" },
|
|
67
68
|
"nb-NO": { label: "Norsk bokm\xE5l" },
|
|
68
69
|
"ne-NP": { label: "\u0928\u0947\u092A\u093E\u0932\u0940" },
|
|
69
70
|
"nl-BE": { label: "Vlaams" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.28",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.1.
|
|
31
|
-
"@nocobase/flow-engine": "2.1.
|
|
32
|
-
"@nocobase/sdk": "2.1.
|
|
33
|
-
"@nocobase/shared": "2.1.
|
|
34
|
-
"@nocobase/utils": "2.1.
|
|
30
|
+
"@nocobase/evaluators": "2.1.28",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.28",
|
|
32
|
+
"@nocobase/sdk": "2.1.28",
|
|
33
|
+
"@nocobase/shared": "2.1.28",
|
|
34
|
+
"@nocobase/utils": "2.1.28",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
"json5": "^2.2.3",
|
|
45
45
|
"jsqr": "^1.4.0",
|
|
46
46
|
"lodash": "4.17.21",
|
|
47
|
+
"react-device-detect": "2.2.3",
|
|
47
48
|
"react-i18next": "^11.15.1",
|
|
48
49
|
"react-router-dom": "^6.30.1"
|
|
49
50
|
},
|
|
50
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "584c0c0e69d32fe502014a6836b3fcb669e89525"
|
|
51
52
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { createMockClient, PluginFlowEngine } from '@nocobase/client-v2';
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
const { detectedDeviceType } = vi.hoisted(() => ({
|
|
14
|
+
detectedDeviceType: { value: 'mobile' },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('react-device-detect', () => ({
|
|
18
|
+
get deviceType() {
|
|
19
|
+
return detectedDeviceType.value;
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('PluginFlowEngine', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
detectedDeviceType.value = 'mobile';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should register the current device type before shared flow components', async () => {
|
|
29
|
+
const app = createMockClient({
|
|
30
|
+
router: {
|
|
31
|
+
type: 'memory',
|
|
32
|
+
initialEntries: ['/'],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const plugin = new PluginFlowEngine({}, app);
|
|
36
|
+
const addComponents = app.addComponents.bind(app);
|
|
37
|
+
const deviceTypesBeforeComponentRegistration: string[] = [];
|
|
38
|
+
vi.spyOn(app, 'addComponents').mockImplementation((components) => {
|
|
39
|
+
deviceTypesBeforeComponentRegistration.push(app.flowEngine.context.deviceType);
|
|
40
|
+
return addComponents(components);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await plugin.load();
|
|
44
|
+
|
|
45
|
+
expect(app.flowEngine.context.deviceType).toBe('mobile');
|
|
46
|
+
expect(deviceTypesBeforeComponentRegistration).toEqual(['mobile']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should normalize the browser device type to computer', async () => {
|
|
50
|
+
detectedDeviceType.value = 'browser';
|
|
51
|
+
const app = createMockClient();
|
|
52
|
+
const plugin = new PluginFlowEngine({}, app);
|
|
53
|
+
|
|
54
|
+
await plugin.load();
|
|
55
|
+
|
|
56
|
+
expect(app.flowEngine.context.deviceType).toBe('computer');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
11
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
-
import { deviceType } from 'react-device-detect';
|
|
13
12
|
import { useParams } from 'react-router-dom';
|
|
14
13
|
import { useApp } from '../../hooks/useApp';
|
|
15
14
|
import { NocoBaseDesktopRouteType } from '../../flow-compat';
|
|
@@ -19,6 +18,7 @@ import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutM
|
|
|
19
18
|
import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
|
|
20
19
|
import { AppNotFound } from '../../components';
|
|
21
20
|
import { useKeepAlive } from '../../components/KeepAlive';
|
|
21
|
+
import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
|
|
22
22
|
|
|
23
23
|
type FlowRouteGuardState = {
|
|
24
24
|
pending: boolean;
|
|
@@ -94,31 +94,9 @@ const BridgeFlowRoute = ({
|
|
|
94
94
|
const layoutContentRef = useRef<HTMLDivElement>(null);
|
|
95
95
|
|
|
96
96
|
useEffect(() => {
|
|
97
|
-
flowEngine.context.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
meta: {
|
|
101
|
-
type: 'string',
|
|
102
|
-
title: flowEngine.translate('Current device type'),
|
|
103
|
-
interface: 'select',
|
|
104
|
-
uiSchema: {
|
|
105
|
-
enum: [
|
|
106
|
-
{ label: flowEngine.translate('Computer'), value: 'computer' },
|
|
107
|
-
{ label: flowEngine.translate('Mobile'), value: 'mobile' },
|
|
108
|
-
{ label: flowEngine.translate('Tablet'), value: 'tablet' },
|
|
109
|
-
{ label: flowEngine.translate('SmartTv'), value: 'smarttv' },
|
|
110
|
-
{ label: flowEngine.translate('Console'), value: 'console' },
|
|
111
|
-
{ label: flowEngine.translate('Wearable'), value: 'wearable' },
|
|
112
|
-
{ label: flowEngine.translate('Embedded'), value: 'embedded' },
|
|
113
|
-
],
|
|
114
|
-
'x-component': 'Select',
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
info: {
|
|
118
|
-
description: 'Current device type (computer/mobile/tablet/...).',
|
|
119
|
-
detail: 'string',
|
|
120
|
-
},
|
|
121
|
-
});
|
|
97
|
+
if (!flowEngine.context.getPropertyOptions('deviceType')) {
|
|
98
|
+
registerDeviceTypeContext(flowEngine);
|
|
99
|
+
}
|
|
122
100
|
}, [flowEngine]);
|
|
123
101
|
|
|
124
102
|
useLayoutRoutePage({
|
|
@@ -136,8 +114,8 @@ const BridgeFlowRoute = ({
|
|
|
136
114
|
/**
|
|
137
115
|
* 管理后台动态页面路由组件。
|
|
138
116
|
*
|
|
139
|
-
* 负责读取当前路由页面 UID
|
|
140
|
-
*
|
|
117
|
+
* 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
|
|
118
|
+
* 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
|
|
141
119
|
*
|
|
142
120
|
* @example
|
|
143
121
|
* ```tsx
|
package/src/flow/index.ts
CHANGED
|
@@ -22,12 +22,14 @@ import { Markdown } from './common/Markdown/Markdown';
|
|
|
22
22
|
import { LiquidEngine } from './common/Liquid';
|
|
23
23
|
import type { PreviewRunJSResult } from './components/code-editor/runjsDiagnostics';
|
|
24
24
|
import { TextAreaWithContextSelector } from './components/TextAreaWithContextSelector';
|
|
25
|
+
import { registerDeviceTypeContext } from './internal/registerDeviceTypeContext';
|
|
25
26
|
|
|
26
27
|
export class PluginFlowEngine<TApp extends BaseApplication<any> = BaseApplication<any>> extends Plugin<
|
|
27
28
|
PluginOptions<any>,
|
|
28
29
|
TApp
|
|
29
30
|
> {
|
|
30
31
|
async load() {
|
|
32
|
+
registerDeviceTypeContext(this.flowEngine);
|
|
31
33
|
this.app.addComponents({ FlowRoute });
|
|
32
34
|
this.app.flowEngine.setModelRepository(new FlowModelRepository(this.app));
|
|
33
35
|
const filteredModels = Object.fromEntries(
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { deviceType } from 'react-device-detect';
|
|
12
|
+
|
|
13
|
+
export const registerDeviceTypeContext = (flowEngine: FlowEngine) => {
|
|
14
|
+
flowEngine.context.defineProperty('deviceType', {
|
|
15
|
+
get: () => (deviceType === 'browser' ? 'computer' : deviceType),
|
|
16
|
+
cache: false,
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
title: flowEngine.translate('Current device type'),
|
|
20
|
+
interface: 'select',
|
|
21
|
+
uiSchema: {
|
|
22
|
+
enum: [
|
|
23
|
+
{ label: flowEngine.translate('Computer'), value: 'computer' },
|
|
24
|
+
{ label: flowEngine.translate('Mobile'), value: 'mobile' },
|
|
25
|
+
{ label: flowEngine.translate('Tablet'), value: 'tablet' },
|
|
26
|
+
{ label: flowEngine.translate('SmartTv'), value: 'smarttv' },
|
|
27
|
+
{ label: flowEngine.translate('Console'), value: 'console' },
|
|
28
|
+
{ label: flowEngine.translate('Wearable'), value: 'wearable' },
|
|
29
|
+
{ label: flowEngine.translate('Embedded'), value: 'embedded' },
|
|
30
|
+
],
|
|
31
|
+
'x-component': 'Select',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
info: {
|
|
35
|
+
description: 'Current device type (computer/mobile/tablet/...).',
|
|
36
|
+
detail: 'string',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
};
|
|
@@ -569,9 +569,13 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
|
|
|
569
569
|
}
|
|
570
570
|
if (fieldPath.includes('.')) {
|
|
571
571
|
// 关系数据
|
|
572
|
+
const collection = this.collection;
|
|
573
|
+
if (!collection) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
572
576
|
const [field1, field2] = fieldPath.split('.');
|
|
573
577
|
const associationField = this.context.dataSourceManager.getCollectionField(
|
|
574
|
-
`${
|
|
578
|
+
`${collection.dataSourceKey}.${collection.name}.${field1}`,
|
|
575
579
|
) as CollectionField;
|
|
576
580
|
if (!associationField) {
|
|
577
581
|
return;
|
|
@@ -583,7 +587,7 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
|
|
|
583
587
|
}
|
|
584
588
|
|
|
585
589
|
const collectionField = this.context.dataSourceManager.getCollectionField(
|
|
586
|
-
`${
|
|
590
|
+
`${collection.dataSourceKey}.${targetCollectionName}.${field2}`,
|
|
587
591
|
) as CollectionField;
|
|
588
592
|
|
|
589
593
|
if (collectionField && collectionField.isAssociationField()) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { FlowModel, FlowModelRenderer, observable, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import { Icon, type NocoBaseDesktopRoute } from '../../../../flow-compat';
|
|
12
|
+
import type { RouteRepository } from '../../../../RouteRepository';
|
|
12
13
|
import { useRequest } from 'ahooks';
|
|
13
14
|
import React from 'react';
|
|
14
15
|
import { SkeletonFallback } from '../../../components/SkeletonFallback';
|
|
@@ -52,6 +53,25 @@ function normalizePersistedRoute(payload: unknown): Partial<NocoBaseDesktopRoute
|
|
|
52
53
|
return undefined;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
type RefreshableRouteRepository = Pick<RouteRepository, 'refreshAccessible'>;
|
|
57
|
+
|
|
58
|
+
const routeRefreshQueues = new WeakMap<RefreshableRouteRepository, Promise<unknown>>();
|
|
59
|
+
|
|
60
|
+
async function refreshRouteRepository(repository: RefreshableRouteRepository) {
|
|
61
|
+
const previous = routeRefreshQueues.get(repository) || Promise.resolve();
|
|
62
|
+
const current = previous.catch(() => undefined).then(() => repository.refreshAccessible());
|
|
63
|
+
|
|
64
|
+
routeRefreshQueues.set(repository, current);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await current;
|
|
68
|
+
} finally {
|
|
69
|
+
if (routeRefreshQueues.get(repository) === current) {
|
|
70
|
+
routeRefreshQueues.delete(repository);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
export class BasePageTabModel extends FlowModel<{
|
|
56
76
|
subModels: {
|
|
57
77
|
grid: BlockGridModel;
|
|
@@ -168,22 +188,36 @@ export class RootPageTabModel extends BasePageTabModel {
|
|
|
168
188
|
async save() {
|
|
169
189
|
const json = this.serialize();
|
|
170
190
|
const documentTitle = this.stepParams?.pageTabSettings?.tab?.documentTitle;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
const route = this.props.route || {};
|
|
192
|
+
const persisted = route.id != null;
|
|
193
|
+
const currentRoute =
|
|
194
|
+
persisted && route.schemaUid ? this.context.routeRepository?.getRouteBySchemaUid?.(route.schemaUid) : undefined;
|
|
195
|
+
const data = {
|
|
196
|
+
...(persisted ? { schemaUid: route.schemaUid } : route),
|
|
197
|
+
title: this.getTabTitle(''),
|
|
198
|
+
icon: this.getTabIcon(),
|
|
199
|
+
options: {
|
|
200
|
+
...(currentRoute ? currentRoute.options : route.options),
|
|
201
|
+
flowRegistry: json.flowRegistry,
|
|
202
|
+
documentTitle,
|
|
176
203
|
},
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
};
|
|
205
|
+
const response = await this.context.api.request(
|
|
206
|
+
persisted
|
|
207
|
+
? {
|
|
208
|
+
method: 'post',
|
|
209
|
+
url: `desktopRoutes:update?filter[id]=${route.id}`,
|
|
210
|
+
data,
|
|
211
|
+
}
|
|
212
|
+
: {
|
|
213
|
+
method: 'post',
|
|
214
|
+
url: 'desktopRoutes:updateOrCreate',
|
|
215
|
+
params: {
|
|
216
|
+
filterKeys: ['schemaUid'],
|
|
217
|
+
},
|
|
218
|
+
data,
|
|
219
|
+
},
|
|
220
|
+
);
|
|
187
221
|
const persistedRoute = normalizePersistedRoute(response?.data?.data);
|
|
188
222
|
|
|
189
223
|
// 新建 tab 首次保存后需要立即拿到持久化 route id,拖拽排序会直接依赖它。
|
|
@@ -197,6 +231,20 @@ export class RootPageTabModel extends BasePageTabModel {
|
|
|
197
231
|
},
|
|
198
232
|
});
|
|
199
233
|
}
|
|
234
|
+
|
|
235
|
+
// 后续保存会从 RouteRepository 读取扩展 options,当前写入完成后必须先同步最新路由快照。
|
|
236
|
+
try {
|
|
237
|
+
if (this.context.routeRepository?.refreshAccessible) {
|
|
238
|
+
await refreshRouteRepository(this.context.routeRepository);
|
|
239
|
+
} else {
|
|
240
|
+
await this.context.refreshDesktopRoutes?.();
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.context.logger?.warn?.(
|
|
244
|
+
{ err: error },
|
|
245
|
+
'[client-v2] Failed to refresh desktop routes after saving a page tab',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
200
248
|
}
|
|
201
249
|
|
|
202
250
|
async destroy() {
|