@nocobase/client-v2 2.2.0-beta.3 → 2.2.0-beta.6
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/RouteRepository.d.ts +21 -6
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +167 -121
- package/lib/index.js +167 -121
- package/package.json +7 -7
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/settings-center.test.tsx +33 -0
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/linkageRules.tsx +122 -34
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/index.ts +1 -1
- package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +12 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
- package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/plugin-manager/index.tsx +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.6",
|
|
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.2.0-beta.
|
|
31
|
-
"@nocobase/flow-engine": "2.2.0-beta.
|
|
32
|
-
"@nocobase/sdk": "2.2.0-beta.
|
|
33
|
-
"@nocobase/shared": "2.2.0-beta.
|
|
34
|
-
"@nocobase/utils": "2.2.0-beta.
|
|
30
|
+
"@nocobase/evaluators": "2.2.0-beta.6",
|
|
31
|
+
"@nocobase/flow-engine": "2.2.0-beta.6",
|
|
32
|
+
"@nocobase/sdk": "2.2.0-beta.6",
|
|
33
|
+
"@nocobase/shared": "2.2.0-beta.6",
|
|
34
|
+
"@nocobase/utils": "2.2.0-beta.6",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"react-i18next": "^11.15.1",
|
|
47
47
|
"react-router-dom": "^6.30.1"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "dc9246516f8a3efd6056f22fe7b3bc947bd4575b"
|
|
50
50
|
}
|
package/src/RouteRepository.ts
CHANGED
|
@@ -9,15 +9,24 @@
|
|
|
9
9
|
|
|
10
10
|
import { APIClient } from '@nocobase/sdk';
|
|
11
11
|
import type { NocoBaseDesktopRoute } from './flow-compat';
|
|
12
|
+
import { ADMIN_LAYOUT_MODEL_UID } from './flow/admin-shell/admin-layout/constants';
|
|
12
13
|
|
|
13
14
|
type RouteSubscriber = () => void;
|
|
14
15
|
type RouteCreateValues = Partial<NocoBaseDesktopRoute>;
|
|
15
16
|
type RouteUpdateValues = Partial<NocoBaseDesktopRoute>;
|
|
17
|
+
type RouteLayoutLike = {
|
|
18
|
+
uid?: unknown;
|
|
19
|
+
};
|
|
16
20
|
|
|
17
21
|
type RouteMutationOptions = {
|
|
18
22
|
refreshAfterMutation?: boolean;
|
|
19
23
|
};
|
|
20
24
|
|
|
25
|
+
type AccessibleRouteCache = {
|
|
26
|
+
accessibleLoaded: boolean;
|
|
27
|
+
routes: NocoBaseDesktopRoute[];
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
type MoveRouteOptions = {
|
|
22
31
|
sourceId: string | number;
|
|
23
32
|
targetId?: string | number;
|
|
@@ -30,21 +39,47 @@ type MoveRouteOptions = {
|
|
|
30
39
|
|
|
31
40
|
export class RouteRepository {
|
|
32
41
|
routes: NocoBaseDesktopRoute[] = [];
|
|
33
|
-
protected subscribers = new
|
|
34
|
-
protected
|
|
35
|
-
|
|
42
|
+
protected subscribers = new Map<RouteSubscriber, string>();
|
|
43
|
+
protected accessibleLoadingPromises = new Map<string, Promise<NocoBaseDesktopRoute[]>>();
|
|
44
|
+
private layoutActivations: Array<{ token: symbol; uid: string }> = [];
|
|
45
|
+
private routeCaches = new Map<string, AccessibleRouteCache>();
|
|
46
|
+
private refreshRequestIds = new Map<string, number>();
|
|
47
|
+
private notifiedLayoutUid: string | undefined;
|
|
36
48
|
|
|
37
49
|
constructor(protected ctx: { api?: APIClient }) {}
|
|
38
50
|
|
|
51
|
+
activateLayout(layout?: RouteLayoutLike) {
|
|
52
|
+
const uid = this.normalizeLayoutUid(layout?.uid);
|
|
53
|
+
const token = Symbol('route-layout');
|
|
54
|
+
|
|
55
|
+
this.layoutActivations.push({
|
|
56
|
+
token,
|
|
57
|
+
uid,
|
|
58
|
+
});
|
|
59
|
+
this.syncRoutesProperty();
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
const index = this.layoutActivations.findIndex((item) => item.token === token);
|
|
63
|
+
if (index >= 0) {
|
|
64
|
+
this.layoutActivations.splice(index, 1);
|
|
65
|
+
}
|
|
66
|
+
this.syncRoutesProperty();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
/**
|
|
40
71
|
* 同步当前可访问桌面路由,并通知订阅方刷新。
|
|
41
72
|
*
|
|
42
73
|
* @param routes 最新的桌面路由树
|
|
43
74
|
*/
|
|
44
|
-
setRoutes(routes: NocoBaseDesktopRoute[]) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
setRoutes(routes: NocoBaseDesktopRoute[], layoutUid = this.getCurrentLayoutUid()) {
|
|
76
|
+
const cache = this.getRouteCache(layoutUid);
|
|
77
|
+
cache.routes = routes;
|
|
78
|
+
cache.accessibleLoaded = true;
|
|
79
|
+
if (layoutUid === this.getCurrentLayoutUid()) {
|
|
80
|
+
this.routes = routes;
|
|
81
|
+
}
|
|
82
|
+
this.emitChange(layoutUid);
|
|
48
83
|
}
|
|
49
84
|
|
|
50
85
|
/**
|
|
@@ -53,7 +88,9 @@ export class RouteRepository {
|
|
|
53
88
|
* @returns 当前缓存的路由数组
|
|
54
89
|
*/
|
|
55
90
|
listAccessible() {
|
|
56
|
-
|
|
91
|
+
const routes = this.getRouteCache(this.getReadableLayoutUid()).routes;
|
|
92
|
+
this.routes = routes;
|
|
93
|
+
return routes;
|
|
57
94
|
}
|
|
58
95
|
|
|
59
96
|
/**
|
|
@@ -62,7 +99,7 @@ export class RouteRepository {
|
|
|
62
99
|
* @returns {boolean} 是否已初始化完成
|
|
63
100
|
*/
|
|
64
101
|
isAccessibleLoaded() {
|
|
65
|
-
return this.accessibleLoaded;
|
|
102
|
+
return this.getRouteCache().accessibleLoaded;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
/**
|
|
@@ -71,12 +108,20 @@ export class RouteRepository {
|
|
|
71
108
|
* @returns 最新的路由数组
|
|
72
109
|
*/
|
|
73
110
|
async refreshAccessible() {
|
|
111
|
+
const layoutUid = this.getCurrentLayoutUid();
|
|
112
|
+
return this.refreshAccessibleByLayout(layoutUid);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async refreshAccessibleByLayout(layoutUid: string) {
|
|
116
|
+
const requestId = this.nextRefreshRequestId(layoutUid);
|
|
74
117
|
const response = await this.getAPIClient().request({
|
|
75
118
|
url: '/desktopRoutes:listAccessible',
|
|
76
|
-
params: { tree: true, sort: 'sort' },
|
|
119
|
+
params: { tree: true, sort: 'sort', layout: layoutUid },
|
|
77
120
|
});
|
|
78
121
|
const routes = response?.data?.data || [];
|
|
79
|
-
this.
|
|
122
|
+
if (requestId === this.getRefreshRequestId(layoutUid)) {
|
|
123
|
+
this.setRoutes(routes, layoutUid);
|
|
124
|
+
}
|
|
80
125
|
return routes;
|
|
81
126
|
}
|
|
82
127
|
|
|
@@ -86,19 +131,26 @@ export class RouteRepository {
|
|
|
86
131
|
* @returns 当前可访问的路由数组
|
|
87
132
|
*/
|
|
88
133
|
async ensureAccessibleLoaded() {
|
|
89
|
-
|
|
90
|
-
|
|
134
|
+
const layoutUid = this.getCurrentLayoutUid();
|
|
135
|
+
const cache = this.getRouteCache(layoutUid);
|
|
136
|
+
if (cache.accessibleLoaded) {
|
|
137
|
+
this.routes = cache.routes;
|
|
138
|
+
return cache.routes;
|
|
91
139
|
}
|
|
92
140
|
|
|
93
|
-
|
|
94
|
-
|
|
141
|
+
const existingLoadingPromise = this.accessibleLoadingPromises.get(layoutUid);
|
|
142
|
+
if (existingLoadingPromise) {
|
|
143
|
+
return existingLoadingPromise;
|
|
95
144
|
}
|
|
96
145
|
|
|
97
|
-
|
|
98
|
-
this.
|
|
146
|
+
const loadingPromise = this.refreshAccessibleByLayout(layoutUid).finally(() => {
|
|
147
|
+
if (this.accessibleLoadingPromises.get(layoutUid) === loadingPromise) {
|
|
148
|
+
this.accessibleLoadingPromises.delete(layoutUid);
|
|
149
|
+
}
|
|
99
150
|
});
|
|
151
|
+
this.accessibleLoadingPromises.set(layoutUid, loadingPromise);
|
|
100
152
|
|
|
101
|
-
return
|
|
153
|
+
return loadingPromise;
|
|
102
154
|
}
|
|
103
155
|
|
|
104
156
|
/**
|
|
@@ -108,7 +160,7 @@ export class RouteRepository {
|
|
|
108
160
|
* @returns 取消订阅函数
|
|
109
161
|
*/
|
|
110
162
|
subscribe(subscriber: RouteSubscriber) {
|
|
111
|
-
this.subscribers.
|
|
163
|
+
this.subscribers.set(subscriber, this.getCurrentLayoutUid());
|
|
112
164
|
return () => {
|
|
113
165
|
this.subscribers.delete(subscriber);
|
|
114
166
|
};
|
|
@@ -132,7 +184,10 @@ export class RouteRepository {
|
|
|
132
184
|
*/
|
|
133
185
|
async createRoute(values: RouteCreateValues, options: RouteMutationOptions = {}) {
|
|
134
186
|
const { refreshAfterMutation = true } = options;
|
|
135
|
-
const res = await this.getResource('desktopRoutes').create({
|
|
187
|
+
const res = await this.getResource('desktopRoutes').create({
|
|
188
|
+
values,
|
|
189
|
+
layout: this.getCurrentLayoutUid(),
|
|
190
|
+
});
|
|
136
191
|
if (refreshAfterMutation) {
|
|
137
192
|
await this.refreshAccessible();
|
|
138
193
|
}
|
|
@@ -210,7 +265,7 @@ export class RouteRepository {
|
|
|
210
265
|
* @returns 匹配到的路由节点
|
|
211
266
|
*/
|
|
212
267
|
getRouteBySchemaUid(schemaUid: string): NocoBaseDesktopRoute | undefined {
|
|
213
|
-
return this.findRoute(this.
|
|
268
|
+
return this.findRoute(this.listAccessible(), schemaUid);
|
|
214
269
|
}
|
|
215
270
|
|
|
216
271
|
protected getAPIClient(): APIClient {
|
|
@@ -224,12 +279,59 @@ export class RouteRepository {
|
|
|
224
279
|
return this.getAPIClient().resource(collectionName);
|
|
225
280
|
}
|
|
226
281
|
|
|
227
|
-
protected emitChange() {
|
|
228
|
-
this.subscribers.forEach((subscriber) => {
|
|
229
|
-
|
|
282
|
+
protected emitChange(layoutUid = this.getCurrentLayoutUid()) {
|
|
283
|
+
this.subscribers.forEach((subscriberLayoutUid, subscriber) => {
|
|
284
|
+
if (subscriberLayoutUid !== layoutUid) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const previousNotifiedLayoutUid = this.notifiedLayoutUid;
|
|
288
|
+
this.notifiedLayoutUid = layoutUid;
|
|
289
|
+
try {
|
|
290
|
+
subscriber();
|
|
291
|
+
} finally {
|
|
292
|
+
this.notifiedLayoutUid = previousNotifiedLayoutUid;
|
|
293
|
+
}
|
|
230
294
|
});
|
|
231
295
|
}
|
|
232
296
|
|
|
297
|
+
private getCurrentLayoutUid() {
|
|
298
|
+
return this.layoutActivations[this.layoutActivations.length - 1]?.uid || ADMIN_LAYOUT_MODEL_UID;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private normalizeLayoutUid(uid: unknown) {
|
|
302
|
+
return typeof uid === 'string' && uid.trim() ? uid : ADMIN_LAYOUT_MODEL_UID;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private getReadableLayoutUid() {
|
|
306
|
+
return this.notifiedLayoutUid || this.getCurrentLayoutUid();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private getRouteCache(layoutUid = this.getCurrentLayoutUid()) {
|
|
310
|
+
let cache = this.routeCaches.get(layoutUid);
|
|
311
|
+
if (!cache) {
|
|
312
|
+
cache = {
|
|
313
|
+
accessibleLoaded: false,
|
|
314
|
+
routes: [],
|
|
315
|
+
};
|
|
316
|
+
this.routeCaches.set(layoutUid, cache);
|
|
317
|
+
}
|
|
318
|
+
return cache;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private syncRoutesProperty() {
|
|
322
|
+
this.routes = this.getRouteCache().routes;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private nextRefreshRequestId(layoutUid: string) {
|
|
326
|
+
const requestId = this.getRefreshRequestId(layoutUid) + 1;
|
|
327
|
+
this.refreshRequestIds.set(layoutUid, requestId);
|
|
328
|
+
return requestId;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private getRefreshRequestId(layoutUid: string) {
|
|
332
|
+
return this.refreshRequestIds.get(layoutUid) || 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
233
335
|
private findRoute(routes: NocoBaseDesktopRoute[], schemaUid: string): NocoBaseDesktopRoute | undefined {
|
|
234
336
|
for (const route of routes) {
|
|
235
337
|
if (route.schemaUid === schemaUid) {
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import type { NocoBaseDesktopRoute } from '../flow-compat';
|
|
12
|
+
import { RouteRepository } from '../RouteRepository';
|
|
13
|
+
|
|
14
|
+
function createRouteRepository() {
|
|
15
|
+
const request = vi.fn().mockResolvedValue({
|
|
16
|
+
data: {
|
|
17
|
+
data: [],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const create = vi.fn().mockResolvedValue({
|
|
21
|
+
data: {
|
|
22
|
+
data: {},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const resource = vi.fn(() => ({
|
|
26
|
+
create,
|
|
27
|
+
}));
|
|
28
|
+
const repository = new RouteRepository({
|
|
29
|
+
api: {
|
|
30
|
+
request,
|
|
31
|
+
resource,
|
|
32
|
+
},
|
|
33
|
+
} as never);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
create,
|
|
37
|
+
repository,
|
|
38
|
+
request,
|
|
39
|
+
resource,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function route(schemaUid: string): NocoBaseDesktopRoute {
|
|
44
|
+
return { schemaUid } as NocoBaseDesktopRoute;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('RouteRepository', () => {
|
|
48
|
+
it('should pass the default admin layout when loading accessible routes', async () => {
|
|
49
|
+
const { repository, request } = createRouteRepository();
|
|
50
|
+
|
|
51
|
+
await repository.refreshAccessible();
|
|
52
|
+
|
|
53
|
+
expect(request).toHaveBeenCalledWith({
|
|
54
|
+
url: '/desktopRoutes:listAccessible',
|
|
55
|
+
params: {
|
|
56
|
+
tree: true,
|
|
57
|
+
sort: 'sort',
|
|
58
|
+
layout: 'admin-layout-model',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should pass the active client-v2 layout when loading accessible routes', async () => {
|
|
64
|
+
const { repository, request } = createRouteRepository();
|
|
65
|
+
const deactivateLayout = repository.activateLayout({
|
|
66
|
+
uid: 'custom-desktop-layout-model',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await repository.refreshAccessible();
|
|
70
|
+
deactivateLayout();
|
|
71
|
+
|
|
72
|
+
expect(request).toHaveBeenCalledWith({
|
|
73
|
+
url: '/desktopRoutes:listAccessible',
|
|
74
|
+
params: {
|
|
75
|
+
tree: true,
|
|
76
|
+
sort: 'sort',
|
|
77
|
+
layout: 'custom-desktop-layout-model',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should reload accessible routes when the active client-v2 layout changes', async () => {
|
|
83
|
+
const { repository, request } = createRouteRepository();
|
|
84
|
+
const deactivateFirstLayout = repository.activateLayout({
|
|
85
|
+
uid: 'first-layout-model',
|
|
86
|
+
});
|
|
87
|
+
await repository.ensureAccessibleLoaded();
|
|
88
|
+
deactivateFirstLayout();
|
|
89
|
+
|
|
90
|
+
const deactivateSecondLayout = repository.activateLayout({
|
|
91
|
+
uid: 'second-layout-model',
|
|
92
|
+
});
|
|
93
|
+
await repository.ensureAccessibleLoaded();
|
|
94
|
+
deactivateSecondLayout();
|
|
95
|
+
|
|
96
|
+
expect(request).toHaveBeenNthCalledWith(1, {
|
|
97
|
+
url: '/desktopRoutes:listAccessible',
|
|
98
|
+
params: {
|
|
99
|
+
tree: true,
|
|
100
|
+
sort: 'sort',
|
|
101
|
+
layout: 'first-layout-model',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(request).toHaveBeenNthCalledWith(2, {
|
|
105
|
+
url: '/desktopRoutes:listAccessible',
|
|
106
|
+
params: {
|
|
107
|
+
tree: true,
|
|
108
|
+
sort: 'sort',
|
|
109
|
+
layout: 'second-layout-model',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should report accessible routes as unloaded when the active layout changes', () => {
|
|
115
|
+
const { repository } = createRouteRepository();
|
|
116
|
+
|
|
117
|
+
repository.setRoutes([], 'first-layout-model');
|
|
118
|
+
const deactivateLayout = repository.activateLayout({
|
|
119
|
+
uid: 'second-layout-model',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(repository.isAccessibleLoaded()).toBe(false);
|
|
123
|
+
deactivateLayout();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should keep accessible route caches isolated by active layout', () => {
|
|
127
|
+
const { repository } = createRouteRepository();
|
|
128
|
+
|
|
129
|
+
repository.setRoutes([route('admin-page')], 'admin-layout-model');
|
|
130
|
+
const deactivateLayout = repository.activateLayout({
|
|
131
|
+
uid: 'custom-desktop-layout-model',
|
|
132
|
+
});
|
|
133
|
+
repository.setRoutes([route('custom-page')], 'custom-desktop-layout-model');
|
|
134
|
+
|
|
135
|
+
expect(repository.listAccessible().map((route) => route.schemaUid)).toEqual(['custom-page']);
|
|
136
|
+
expect(repository.getRouteBySchemaUid('admin-page')).toBeUndefined();
|
|
137
|
+
deactivateLayout();
|
|
138
|
+
|
|
139
|
+
expect(repository.listAccessible().map((route) => route.schemaUid)).toEqual(['admin-page']);
|
|
140
|
+
expect(repository.getRouteBySchemaUid('custom-page')).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should notify only subscribers for the changed layout cache', () => {
|
|
144
|
+
const { repository } = createRouteRepository();
|
|
145
|
+
const adminEvents: string[][] = [];
|
|
146
|
+
const customEvents: string[][] = [];
|
|
147
|
+
|
|
148
|
+
const unsubscribeAdmin = repository.subscribe(() => {
|
|
149
|
+
adminEvents.push(repository.listAccessible().map((route) => route.schemaUid || ''));
|
|
150
|
+
});
|
|
151
|
+
const deactivateLayout = repository.activateLayout({
|
|
152
|
+
uid: 'custom-desktop-layout-model',
|
|
153
|
+
});
|
|
154
|
+
const unsubscribeCustom = repository.subscribe(() => {
|
|
155
|
+
customEvents.push(repository.listAccessible().map((route) => route.schemaUid || ''));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
repository.setRoutes([route('custom-page')], 'custom-desktop-layout-model');
|
|
159
|
+
|
|
160
|
+
expect(adminEvents).toEqual([]);
|
|
161
|
+
expect(customEvents).toEqual([['custom-page']]);
|
|
162
|
+
|
|
163
|
+
deactivateLayout();
|
|
164
|
+
repository.setRoutes([route('admin-page')], 'admin-layout-model');
|
|
165
|
+
|
|
166
|
+
expect(adminEvents).toEqual([['admin-page']]);
|
|
167
|
+
expect(customEvents).toEqual([['custom-page']]);
|
|
168
|
+
|
|
169
|
+
unsubscribeAdmin();
|
|
170
|
+
unsubscribeCustom();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should pass the default admin layout when creating a route', async () => {
|
|
174
|
+
const { repository, create } = createRouteRepository();
|
|
175
|
+
|
|
176
|
+
await repository.createRoute(
|
|
177
|
+
{
|
|
178
|
+
title: 'Admin page',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
refreshAfterMutation: false,
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(create).toHaveBeenCalledWith({
|
|
186
|
+
values: {
|
|
187
|
+
title: 'Admin page',
|
|
188
|
+
},
|
|
189
|
+
layout: 'admin-layout-model',
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should pass the active client-v2 layout when creating a route', async () => {
|
|
194
|
+
const { repository, create } = createRouteRepository();
|
|
195
|
+
const deactivateLayout = repository.activateLayout({
|
|
196
|
+
uid: 'custom-desktop-layout-model',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await repository.createRoute(
|
|
200
|
+
{
|
|
201
|
+
title: 'Custom page',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
refreshAfterMutation: false,
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
deactivateLayout();
|
|
208
|
+
|
|
209
|
+
expect(create).toHaveBeenCalledWith({
|
|
210
|
+
values: {
|
|
211
|
+
title: 'Custom page',
|
|
212
|
+
},
|
|
213
|
+
layout: 'custom-desktop-layout-model',
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
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 { IconPicker } from '..';
|
|
11
|
+
|
|
12
|
+
describe('@nocobase/client-v2 exports', () => {
|
|
13
|
+
it('should export IconPicker from the root entry', () => {
|
|
14
|
+
expect(IconPicker).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -354,6 +354,39 @@ describe('settings center', () => {
|
|
|
354
354
|
expect(screen.getByRole('menuitem', { name: 'Menu ACL Demo' })).toBeInTheDocument();
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
it('should allow the settings sidebar menu to scroll independently', async () => {
|
|
358
|
+
class ManySettingsPlugin extends Plugin {
|
|
359
|
+
async load() {
|
|
360
|
+
for (let index = 0; index < 30; index += 1) {
|
|
361
|
+
this.pluginSettingsManager.addMenuItem({
|
|
362
|
+
key: `scroll-demo-${index}`,
|
|
363
|
+
title: `Scroll demo ${index}`,
|
|
364
|
+
});
|
|
365
|
+
this.pluginSettingsManager.addPageTabItem({
|
|
366
|
+
menuKey: `scroll-demo-${index}`,
|
|
367
|
+
key: 'index',
|
|
368
|
+
title: `Scroll demo ${index}`,
|
|
369
|
+
Component: () => <div>{`Scroll demo page ${index}`}</div>,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const app = createMockClient({
|
|
376
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ManySettingsPlugin],
|
|
377
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/scroll-demo-29'] },
|
|
378
|
+
});
|
|
379
|
+
mockAdminRuntime(app);
|
|
380
|
+
|
|
381
|
+
await renderApp(app);
|
|
382
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check']);
|
|
383
|
+
|
|
384
|
+
expect(await screen.findByText('Scroll demo page 29')).toBeInTheDocument();
|
|
385
|
+
|
|
386
|
+
const sidebar = screen.getByRole('menuitem', { name: 'Scroll demo 29' }).closest('.ant-layout-sider');
|
|
387
|
+
expect(sidebar).toHaveStyle({ overflowY: 'auto' });
|
|
388
|
+
});
|
|
389
|
+
|
|
357
390
|
it('should save system settings through systemSettings:put', async () => {
|
|
358
391
|
const app = createMockClient({
|
|
359
392
|
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import { css, cx } from '@emotion/css';
|
|
11
11
|
import { Input, Typography } from 'antd';
|
|
12
12
|
import type { TextAreaProps } from 'antd/es/input';
|
|
13
|
+
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
|
13
14
|
import JSON5 from 'json5';
|
|
14
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import React, { useCallback, useEffect, useMemo, useState, type ChangeEvent, type FocusEvent } from 'react';
|
|
15
16
|
|
|
16
17
|
export interface JsonTextAreaProps extends Omit<TextAreaProps, 'value' | 'onChange'> {
|
|
17
18
|
value?: unknown;
|
|
@@ -36,14 +37,14 @@ function stringifyJsonValue(value: unknown, json: typeof JSON | typeof JSON5, sp
|
|
|
36
37
|
json.parse(value);
|
|
37
38
|
return value;
|
|
38
39
|
} catch {
|
|
39
|
-
return json.stringify(value, undefined, space);
|
|
40
|
+
return json.stringify(value, undefined, space) ?? '';
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
return json.stringify(value, undefined, space);
|
|
44
|
+
return json.stringify(value, undefined, space) ?? '';
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
const JsonTextAreaComponent = React.forwardRef<TextAreaRef, JsonTextAreaProps>((props, ref) => {
|
|
47
48
|
const {
|
|
48
49
|
value,
|
|
49
50
|
onChange,
|
|
@@ -74,11 +75,8 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
74
75
|
[json],
|
|
75
76
|
);
|
|
76
77
|
|
|
77
|
-
const
|
|
78
|
-
(
|
|
79
|
-
const nextText = event.target.value;
|
|
80
|
-
setText(nextText);
|
|
81
|
-
|
|
78
|
+
const validateText = useCallback(
|
|
79
|
+
(nextText: string) => {
|
|
82
80
|
try {
|
|
83
81
|
parseText(nextText);
|
|
84
82
|
setError(undefined);
|
|
@@ -89,12 +87,21 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
89
87
|
[parseText],
|
|
90
88
|
);
|
|
91
89
|
|
|
90
|
+
const handleChange = useCallback(
|
|
91
|
+
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
92
|
+
const nextText = event.target.value;
|
|
93
|
+
setText(nextText);
|
|
94
|
+
validateText(nextText);
|
|
95
|
+
},
|
|
96
|
+
[validateText],
|
|
97
|
+
);
|
|
98
|
+
|
|
92
99
|
const handleBlur = useCallback(
|
|
93
|
-
(event:
|
|
100
|
+
(event: FocusEvent<HTMLTextAreaElement>) => {
|
|
94
101
|
try {
|
|
95
102
|
const parsed = parseText(event.target.value);
|
|
96
103
|
setError(undefined);
|
|
97
|
-
setText(parsed == null ? '' : json.stringify(parsed, undefined, space));
|
|
104
|
+
setText(parsed == null ? '' : json.stringify(parsed, undefined, space) ?? '');
|
|
98
105
|
onChange?.(parsed);
|
|
99
106
|
} catch (err) {
|
|
100
107
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -111,6 +118,7 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
111
118
|
<>
|
|
112
119
|
<Input.TextArea
|
|
113
120
|
{...textAreaProps}
|
|
121
|
+
ref={ref}
|
|
114
122
|
value={text}
|
|
115
123
|
onChange={handleChange}
|
|
116
124
|
onBlur={handleBlur}
|
|
@@ -126,4 +134,6 @@ export const JsonTextArea = React.memo((props: JsonTextAreaProps) => {
|
|
|
126
134
|
);
|
|
127
135
|
});
|
|
128
136
|
|
|
137
|
+
export const JsonTextArea = React.memo(JsonTextAreaComponent);
|
|
138
|
+
|
|
129
139
|
JsonTextArea.displayName = 'JsonTextArea';
|