@nocobase/client-v2 2.1.0-beta.36 → 2.1.0-beta.38
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/Application.d.ts +1 -0
- package/es/BaseApplication.d.ts +4 -0
- package/es/RouterManager.d.ts +1 -0
- package/es/components/KeepAlive.d.ts +22 -0
- package/es/components/RouterBridge.d.ts +9 -0
- package/es/components/form/DialogFormLayout.d.ts +5 -29
- package/es/components/form/filter/CollectionFilter.d.ts +41 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +41 -0
- package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
- package/es/components/form/filter/FilterValueInput.d.ts +29 -0
- package/es/components/form/filter/index.d.ts +11 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
- package/es/data-source/index.d.ts +9 -0
- package/es/flow/FlowPage.d.ts +2 -1
- package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
- package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
- package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
- package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
- package/es/flow/components/FlowRoute.d.ts +10 -1
- package/es/flow/components/filter/index.d.ts +2 -0
- package/es/flow/components/filter/useFilterOptions.d.ts +54 -0
- package/es/flow/index.d.ts +4 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +2 -0
- package/es/index.mjs +491 -439
- package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
- package/es/layout-manager/LayoutManager.d.ts +22 -0
- package/es/layout-manager/LayoutRoute.d.ts +14 -0
- package/es/layout-manager/index.d.ts +13 -0
- package/es/layout-manager/types.d.ts +20 -0
- package/es/layout-manager/utils.d.ts +14 -0
- package/es/nocobase-buildin-plugin/index.d.ts +3 -10
- package/es/settings-center/index.d.ts +1 -1
- package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
- package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
- package/es/settings-center/plugin-manager/types.d.ts +34 -0
- package/lib/index.js +491 -439
- package/package.json +8 -7
- package/src/Application.tsx +27 -12
- package/src/BaseApplication.tsx +19 -0
- package/src/PluginSettingsManager.ts +1 -1
- package/src/RouterManager.tsx +17 -1
- package/src/__tests__/PluginSettingsManager.test.ts +41 -2
- package/src/__tests__/app.test.tsx +17 -1
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
- package/src/__tests__/plugin-manager.test.tsx +177 -0
- package/src/__tests__/settings-center.test.tsx +24 -2
- package/src/components/KeepAlive.tsx +131 -0
- package/src/components/README.md +89 -6
- package/src/components/README.zh-CN.md +89 -7
- package/src/components/RouterBridge.tsx +28 -4
- package/src/components/__tests__/KeepAlive.test.tsx +63 -0
- package/src/components/__tests__/RouterBridge.test.tsx +27 -0
- package/src/components/form/DialogFormLayout.tsx +5 -29
- package/src/components/form/filter/CollectionFilter.tsx +101 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +176 -0
- package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
- package/src/components/form/filter/FilterValueInput.tsx +198 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +205 -0
- package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
- package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
- package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
- package/src/components/form/filter/index.ts +13 -0
- package/src/components/form/filter/useFilterActionProps.ts +200 -0
- package/src/components/form/index.tsx +1 -0
- package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
- package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
- package/src/data-source/index.ts +10 -0
- package/src/flow/FlowPage.tsx +35 -7
- package/src/flow/__tests__/FlowPage.test.tsx +79 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
- package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
- package/src/flow/actions/aclCheck.tsx +4 -0
- package/src/flow/actions/aclCheckRefresh.tsx +4 -0
- package/src/flow/actions/dateTimeFormat.tsx +12 -8
- package/src/flow/actions/linkageRules.tsx +122 -0
- package/src/flow/actions/openView.tsx +28 -4
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
- package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
- package/src/flow/admin-shell/admin-layout/index.ts +2 -0
- package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
- package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
- package/src/flow/components/AdminLayout.tsx +4 -154
- package/src/flow/components/FlowRoute.tsx +105 -15
- package/src/flow/components/filter/index.ts +3 -0
- package/src/flow/components/filter/useFilterOptions.ts +80 -0
- package/src/flow/index.ts +4 -0
- package/src/flow/models/base/ActionModel.tsx +8 -1
- package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
- package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
- package/src/flow/models/base/RouteModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
- package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
- package/src/flow/models/blocks/form/submitValues.ts +4 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
- package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
- package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
- package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
- package/src/index.ts +2 -0
- package/src/layout-manager/LayoutContentRoute.tsx +90 -0
- package/src/layout-manager/LayoutManager.tsx +185 -0
- package/src/layout-manager/LayoutRoute.tsx +138 -0
- package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
- package/src/layout-manager/index.ts +14 -0
- package/src/layout-manager/types.ts +22 -0
- package/src/layout-manager/utils.ts +37 -0
- package/src/nocobase-buildin-plugin/index.tsx +69 -67
- package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
- package/src/settings-center/index.ts +1 -1
- package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
- package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
- package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
- package/src/settings-center/plugin-manager/index.tsx +254 -0
- package/src/settings-center/plugin-manager/types.ts +35 -0
- package/src/settings-center/utils.tsx +8 -1
- package/src/theme/__tests__/globalStyles.test.ts +24 -0
- package/src/theme/globalStyles.ts +10 -0
- package/src/utils/globalDeps.ts +2 -0
- package/src/settings-center/PluginManagerPage.tsx +0 -162
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.38",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -20,17 +20,18 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@ant-design/icons": "^5.6.1",
|
|
23
|
+
"@ctrl/tinycolor": "^3.6.0",
|
|
23
24
|
"@dnd-kit/core": "^6.0.0",
|
|
24
25
|
"@dnd-kit/sortable": "^7.0.0",
|
|
25
26
|
"@emotion/css": "^11.7.1",
|
|
26
27
|
"@formily/antd-v5": "1.2.3",
|
|
27
28
|
"@formily/react": "^2.2.27",
|
|
28
29
|
"@formily/shared": "^2.2.27",
|
|
29
|
-
"@nocobase/evaluators": "2.1.0-beta.
|
|
30
|
-
"@nocobase/flow-engine": "2.1.0-beta.
|
|
31
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
32
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
33
|
-
"@nocobase/utils": "2.1.0-beta.
|
|
30
|
+
"@nocobase/evaluators": "2.1.0-beta.38",
|
|
31
|
+
"@nocobase/flow-engine": "2.1.0-beta.38",
|
|
32
|
+
"@nocobase/sdk": "2.1.0-beta.38",
|
|
33
|
+
"@nocobase/shared": "2.1.0-beta.38",
|
|
34
|
+
"@nocobase/utils": "2.1.0-beta.38",
|
|
34
35
|
"ahooks": "^3.7.2",
|
|
35
36
|
"antd": "5.24.2",
|
|
36
37
|
"antd-style": "3.7.1",
|
|
@@ -44,5 +45,5 @@
|
|
|
44
45
|
"react-i18next": "^11.15.1",
|
|
45
46
|
"react-router-dom": "^6.30.1"
|
|
46
47
|
},
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "d1c585108ff6e51c17b0b52bacb1a2d621d9c119"
|
|
48
49
|
}
|
package/src/Application.tsx
CHANGED
|
@@ -42,6 +42,7 @@ export class Application extends BaseApplication<
|
|
|
42
42
|
PluginSettingsManager
|
|
43
43
|
> {
|
|
44
44
|
public declare dataSourceManager: any;
|
|
45
|
+
public hasLoadError = false;
|
|
45
46
|
|
|
46
47
|
protected createApiClient(options: ApplicationOptions) {
|
|
47
48
|
return new APIClient({
|
|
@@ -99,9 +100,29 @@ export class Application extends BaseApplication<
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
async load() {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
try {
|
|
104
|
+
this.hasLoadError = false;
|
|
105
|
+
await this.loadWebSocket();
|
|
106
|
+
await this.pm.load();
|
|
107
|
+
await this.flowEngine.flowSettings.load();
|
|
108
|
+
} catch (error: any) {
|
|
109
|
+
this.hasLoadError = true;
|
|
110
|
+
|
|
111
|
+
if (error?.response?.data?.errors?.[0]?.code === 'BLOCKED_IP') {
|
|
112
|
+
this.hasLoadError = false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.ws.enabled) {
|
|
116
|
+
await new Promise((resolve) => {
|
|
117
|
+
setTimeout(() => resolve(null), 1000);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
this.error = {
|
|
121
|
+
code: 'LOAD_ERROR',
|
|
122
|
+
...this.apiClient.toErrMessages(error)?.[0],
|
|
123
|
+
};
|
|
124
|
+
console.error(error, this.error);
|
|
125
|
+
}
|
|
105
126
|
this.updateFavicon();
|
|
106
127
|
}
|
|
107
128
|
|
|
@@ -130,20 +151,14 @@ export class Application extends BaseApplication<
|
|
|
130
151
|
return;
|
|
131
152
|
}
|
|
132
153
|
|
|
133
|
-
if (this.error && data.payload.code === 'APP_RUNNING') {
|
|
134
|
-
this.maintained = true;
|
|
135
|
-
this.setMaintaining(false);
|
|
136
|
-
this.error = null;
|
|
137
|
-
globalThis.window.location.reload();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
154
|
const maintaining = data.type === 'maintaining' && data.payload.code !== 'APP_RUNNING';
|
|
142
|
-
console.log('ws:message', { maintaining, data });
|
|
143
155
|
if (maintaining) {
|
|
144
156
|
this.setMaintaining(true);
|
|
145
157
|
this.error = data.payload;
|
|
146
158
|
} else {
|
|
159
|
+
if (this.hasLoadError) {
|
|
160
|
+
globalThis.window.location.reload();
|
|
161
|
+
}
|
|
147
162
|
this.setMaintaining(false);
|
|
148
163
|
this.maintained = true;
|
|
149
164
|
this.error = null;
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import { GlobalThemeProvider } from './theme';
|
|
|
30
30
|
import { AIManager } from './ai';
|
|
31
31
|
import { AppError, AppMaintaining, AppMaintainingDialog, AppNotFound, AppSpin, BlankComponent } from './components';
|
|
32
32
|
import { SystemSettingsSource } from './flow/system-settings';
|
|
33
|
+
import { LayoutManager } from './layout-manager/LayoutManager';
|
|
33
34
|
import type { PluginClass, PluginManager, PluginType } from './PluginManager';
|
|
34
35
|
import { RouteRepository } from './RouteRepository';
|
|
35
36
|
import type {
|
|
@@ -122,6 +123,7 @@ export abstract class BaseApplication<
|
|
|
122
123
|
public components: Record<string, AnyComponent> = {};
|
|
123
124
|
public pluginManager: TPluginManager;
|
|
124
125
|
public pluginSettingsManager: TPluginSettingsManager;
|
|
126
|
+
public layoutManager: LayoutManager<this>;
|
|
125
127
|
public aiManager!: AIManager;
|
|
126
128
|
public devDynamicImport?: DevDynamicImport;
|
|
127
129
|
public requirejs!: RequireJS;
|
|
@@ -169,6 +171,18 @@ export abstract class BaseApplication<
|
|
|
169
171
|
return this.wsAuthorized;
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
public setDocumentLanguage(language?: string | null) {
|
|
175
|
+
if (typeof document === 'undefined') {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (language) {
|
|
180
|
+
document.documentElement.lang = language;
|
|
181
|
+
} else {
|
|
182
|
+
document.documentElement.removeAttribute('lang');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
constructor(protected options: TOptions = {} as TOptions) {
|
|
173
187
|
this.initRequireJs();
|
|
174
188
|
this.defineObservableState();
|
|
@@ -180,6 +194,7 @@ export abstract class BaseApplication<
|
|
|
180
194
|
this.initializeExtendedState();
|
|
181
195
|
this.i18n = this.createI18n(options);
|
|
182
196
|
this.router = this.createRouterManager(options);
|
|
197
|
+
this.layoutManager = this.createLayoutManager(options);
|
|
183
198
|
this.pluginManager = this.createPluginManager(options);
|
|
184
199
|
this.flowEngine = new FlowEngine();
|
|
185
200
|
this.flowEngine.registerModels({ ApplicationModel });
|
|
@@ -205,6 +220,7 @@ export abstract class BaseApplication<
|
|
|
205
220
|
this.addRoutes();
|
|
206
221
|
this.i18n.on('languageChanged', (lng) => {
|
|
207
222
|
this.apiClient.auth.locale = lng;
|
|
223
|
+
this.setDocumentLanguage(lng);
|
|
208
224
|
});
|
|
209
225
|
this.initListeners();
|
|
210
226
|
this.afterManagersInitialized();
|
|
@@ -554,6 +570,9 @@ export abstract class BaseApplication<
|
|
|
554
570
|
protected abstract createRouterManager(options: TOptions): TRouterManager;
|
|
555
571
|
protected abstract createPluginManager(options: TOptions): TPluginManager;
|
|
556
572
|
protected abstract createPluginSettingsManager(options: TOptions): TPluginSettingsManager;
|
|
573
|
+
protected createLayoutManager(_options: TOptions) {
|
|
574
|
+
return new LayoutManager(this);
|
|
575
|
+
}
|
|
557
576
|
protected createWebSocketClient(options: TOptions) {
|
|
558
577
|
return new WebSocketClient(options.ws ?? false);
|
|
559
578
|
}
|
|
@@ -524,7 +524,7 @@ export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseAppli
|
|
|
524
524
|
|
|
525
525
|
if (page.key === 'index') {
|
|
526
526
|
this.app.router.add(this.getRouteName(page.name), {
|
|
527
|
-
|
|
527
|
+
path: '',
|
|
528
528
|
Component: fallbackComponent,
|
|
529
529
|
componentLoader: page.componentLoader,
|
|
530
530
|
});
|
package/src/RouterManager.tsx
CHANGED
|
@@ -50,10 +50,25 @@ export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
|
|
|
50
50
|
Component?: ComponentTypeAndString;
|
|
51
51
|
componentLoader?: ComponentLoader;
|
|
52
52
|
skipAuthCheck?: boolean;
|
|
53
|
+
authCheck?: boolean;
|
|
53
54
|
}
|
|
54
55
|
export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
|
|
55
56
|
export type RouterComponentType = React.FC<{ BaseLayout?: ComponentType }>;
|
|
56
57
|
|
|
58
|
+
function removeBasename(pathname: string, basename?: string) {
|
|
59
|
+
if (!basename || basename === '/') {
|
|
60
|
+
return pathname;
|
|
61
|
+
}
|
|
62
|
+
const normalizedBasename = basename.replace(/\/$/, '');
|
|
63
|
+
if (pathname === normalizedBasename) {
|
|
64
|
+
return '/';
|
|
65
|
+
}
|
|
66
|
+
if (pathname.startsWith(`${normalizedBasename}/`)) {
|
|
67
|
+
return pathname.slice(normalizedBasename.length) || '/';
|
|
68
|
+
}
|
|
69
|
+
return pathname;
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<any>> {
|
|
58
73
|
protected routes: Record<string, RouteType> = {};
|
|
59
74
|
protected options: RouterOptions;
|
|
@@ -186,8 +201,9 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
|
|
|
186
201
|
|
|
187
202
|
matchRoutes(pathname: string) {
|
|
188
203
|
const routes = this.getRoutesTree();
|
|
204
|
+
const basename = this.router?.basename || this.getBasename();
|
|
189
205
|
// @ts-ignore
|
|
190
|
-
return matchRoutes<RouteType>(routes, pathname,
|
|
206
|
+
return matchRoutes<RouteType>(routes, removeBasename(pathname, basename));
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
isSkippedAuthCheckRoute(pathname: string) {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { createMockClient } from '@nocobase/client-v2';
|
|
12
|
+
import { createMemoryRouter } from 'react-router-dom';
|
|
13
|
+
import type { RouteObject } from 'react-router-dom';
|
|
12
14
|
|
|
13
15
|
describe('PluginSettingsManager v2', () => {
|
|
14
16
|
it('should return menu -> page two-level structure', () => {
|
|
@@ -72,7 +74,7 @@ describe('PluginSettingsManager v2', () => {
|
|
|
72
74
|
expect(app.pluginSettingsManager.getRoutePath('demo.advanced')).toBe('/admin/settings/demo/advanced');
|
|
73
75
|
|
|
74
76
|
expect(app.router.get('admin.settings.demo')).toMatchObject({ path: 'demo' });
|
|
75
|
-
expect(app.router.get('admin.settings.demo.index')).toMatchObject({
|
|
77
|
+
expect(app.router.get('admin.settings.demo.index')).toMatchObject({ path: '' });
|
|
76
78
|
expect(app.router.get('admin.settings.demo.advanced')).toMatchObject({ path: 'advanced' });
|
|
77
79
|
});
|
|
78
80
|
|
|
@@ -110,7 +112,44 @@ describe('PluginSettingsManager v2', () => {
|
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
expect(app.pluginSettingsManager.get('demo.index')).toMatchObject({ componentLoader });
|
|
113
|
-
expect(app.router.get('admin.settings.demo.index')).toMatchObject({ componentLoader,
|
|
115
|
+
expect(app.router.get('admin.settings.demo.index')).toMatchObject({ componentLoader, path: '' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should allow nested routes under index page route', () => {
|
|
119
|
+
const app = createMockClient();
|
|
120
|
+
const findRoute = (routes: RouteObject[], routeId: string): RouteObject | null => {
|
|
121
|
+
for (const route of routes) {
|
|
122
|
+
if (route.id === routeId) {
|
|
123
|
+
return route;
|
|
124
|
+
}
|
|
125
|
+
const matched = route.children ? findRoute(route.children, routeId) : null;
|
|
126
|
+
if (matched) {
|
|
127
|
+
return matched;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
app.pluginSettingsManager.addMenuItem({ key: 'demo', title: 'Demo' });
|
|
134
|
+
app.pluginSettingsManager.addPageTabItem({ menuKey: 'demo', key: 'index', title: 'Overview' });
|
|
135
|
+
app.router.add('admin.settings.demo.index.layout', {
|
|
136
|
+
path: 'configure',
|
|
137
|
+
Component: () => React.createElement('div', null, 'configure'),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const routes = app.router.getRoutesTree();
|
|
141
|
+
const indexRoute = findRoute(routes, 'admin.settings.demo.index');
|
|
142
|
+
|
|
143
|
+
expect(indexRoute).toMatchObject({
|
|
144
|
+
id: 'admin.settings.demo.index',
|
|
145
|
+
path: '',
|
|
146
|
+
});
|
|
147
|
+
expect(indexRoute).not.toHaveProperty('index');
|
|
148
|
+
expect(findRoute(routes, 'admin.settings.demo.index.layout')).toMatchObject({
|
|
149
|
+
id: 'admin.settings.demo.index.layout',
|
|
150
|
+
path: 'configure',
|
|
151
|
+
});
|
|
152
|
+
expect(() => createMemoryRouter(routes, { initialEntries: ['/demo/configure/form-1'] })).not.toThrow();
|
|
114
153
|
});
|
|
115
154
|
|
|
116
155
|
it('should merge duplicate registration and refresh route config', () => {
|
|
@@ -33,6 +33,7 @@ describe('app', () => {
|
|
|
33
33
|
|
|
34
34
|
afterEach(() => {
|
|
35
35
|
document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
|
|
36
|
+
document.documentElement.removeAttribute('lang');
|
|
36
37
|
vi.restoreAllMocks();
|
|
37
38
|
});
|
|
38
39
|
|
|
@@ -89,6 +90,14 @@ describe('app', () => {
|
|
|
89
90
|
expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
|
|
90
91
|
});
|
|
91
92
|
|
|
93
|
+
it('should sync document language when app language changes', async () => {
|
|
94
|
+
const app = new Application({ router });
|
|
95
|
+
|
|
96
|
+
await app.i18n.changeLanguage('ja-JP');
|
|
97
|
+
|
|
98
|
+
expect(document.documentElement.lang).toBe('ja-JP');
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
it('should escape app version html placeholder content', () => {
|
|
93
102
|
expect(getAppVersionHTML('<script>alert(1)</script>&"')).toBe(
|
|
94
103
|
'<span class="nb-app-version">v<script>alert(1)</script>&"</span>',
|
|
@@ -234,6 +243,10 @@ describe('app', () => {
|
|
|
234
243
|
});
|
|
235
244
|
await renderApp(app);
|
|
236
245
|
expect(await screen.findByText('Hello Basename Route')).toBeInTheDocument();
|
|
246
|
+
expect(app.router.matchRoutes('/v2/demo/app-info')?.some((match) => match.route.path === '/demo/app-info')).toBe(
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
expect(app.router.matchRoutes('/demo/app-info')?.some((match) => match.route.path === '/demo/app-info')).toBe(true);
|
|
237
250
|
});
|
|
238
251
|
|
|
239
252
|
it('should support plugin settings componentLoader lazy functionality', async () => {
|
|
@@ -370,7 +383,10 @@ describe('app', () => {
|
|
|
370
383
|
|
|
371
384
|
await waitFor(() => expect(screen.queryByText('maintaining error message')).not.toBeInTheDocument());
|
|
372
385
|
expect(screen.getByText('Hello')).toBeInTheDocument();
|
|
373
|
-
|
|
386
|
+
// Aligned with v1: a routine maintaining→APP_RUNNING cycle does not
|
|
387
|
+
// reload the page. Only `hasLoadError === true` (set when the initial
|
|
388
|
+
// `app.load()` itself fails) triggers a recovery reload.
|
|
389
|
+
expect(reloadMock).not.toHaveBeenCalled();
|
|
374
390
|
} finally {
|
|
375
391
|
Object.defineProperty(globalThis.window, 'location', {
|
|
376
392
|
configurable: true,
|
|
@@ -28,6 +28,7 @@ describe('client-v2 defineGlobalDeps', () => {
|
|
|
28
28
|
expect(define).toHaveBeenCalledWith('@nocobase/evaluators/client', expect.any(Function));
|
|
29
29
|
expect(define).toHaveBeenCalledWith('@dnd-kit/core', expect.any(Function));
|
|
30
30
|
expect(define).toHaveBeenCalledWith('@dnd-kit/sortable', expect.any(Function));
|
|
31
|
+
expect(define).toHaveBeenCalledWith('@ctrl/tinycolor', expect.any(Function));
|
|
31
32
|
expect(define).toHaveBeenCalledWith('ahooks', expect.any(Function));
|
|
32
33
|
expect(define).toHaveBeenCalledWith('dayjs', expect.any(Function));
|
|
33
34
|
expect(define).toHaveBeenCalledWith('lodash', expect.any(Function));
|
|
@@ -7,11 +7,21 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createMockClient } from '@nocobase/client-v2';
|
|
11
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
10
|
+
import { createMockClient, Plugin } from '@nocobase/client-v2';
|
|
11
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
|
|
14
14
|
|
|
15
|
+
class SkippedPublicRoutePlugin extends Plugin {
|
|
16
|
+
async load() {
|
|
17
|
+
this.router.add('public', {
|
|
18
|
+
path: '/public',
|
|
19
|
+
skipAuthCheck: true,
|
|
20
|
+
Component: () => <div>public page</div>,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
describe('nocobase buildin plugin auth redirect', () => {
|
|
16
26
|
const originalLocation = globalThis.window.location;
|
|
17
27
|
|
|
@@ -107,6 +117,39 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
107
117
|
});
|
|
108
118
|
});
|
|
109
119
|
|
|
120
|
+
it('should check current user after navigating from skipped route to v2 admin', async () => {
|
|
121
|
+
const app = createMockClient({
|
|
122
|
+
publicPath: '/v2/',
|
|
123
|
+
plugins: [NocoBaseBuildInPlugin as any, SkippedPublicRoutePlugin as any],
|
|
124
|
+
router: { type: 'memory', initialEntries: ['/v2/public'] },
|
|
125
|
+
});
|
|
126
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
127
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
128
|
+
});
|
|
129
|
+
app.apiMock.onGet('/auth:check').reply(200, { data: {} });
|
|
130
|
+
|
|
131
|
+
const Root = app.getRootComponent();
|
|
132
|
+
render(<Root />);
|
|
133
|
+
|
|
134
|
+
expect(await screen.findByText('public page')).toBeInTheDocument();
|
|
135
|
+
const authCheckRequestsBeforeNavigation = app.apiMock.history.get.filter(
|
|
136
|
+
(request) => request.url === '/auth:check',
|
|
137
|
+
).length;
|
|
138
|
+
expect(authCheckRequestsBeforeNavigation).toBe(0);
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
await app.router.router.navigate('/v2/admin');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await waitFor(() => {
|
|
145
|
+
expect(app.apiMock.history.get.filter((request) => request.url === '/auth:check').length).toBeGreaterThan(
|
|
146
|
+
authCheckRequestsBeforeNavigation,
|
|
147
|
+
);
|
|
148
|
+
expect(app.router.router.state.location.pathname).toBe('/v2/signin');
|
|
149
|
+
expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fadmin');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
110
153
|
it('should render v2 admin root without redirecting away', async () => {
|
|
111
154
|
const app = createMockClient({
|
|
112
155
|
publicPath: '/v2/',
|
|
@@ -0,0 +1,177 @@
|
|
|
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 { ACLRolesCheckProvider, createMockClient, Plugin } from '@nocobase/client-v2';
|
|
11
|
+
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
|
|
14
|
+
|
|
15
|
+
class TestAclPlugin extends Plugin {
|
|
16
|
+
async load() {
|
|
17
|
+
this.app.use(ACLRolesCheckProvider);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type MockClientApplication = ReturnType<typeof createMockClient>;
|
|
22
|
+
|
|
23
|
+
const renderApp = (app: MockClientApplication) => {
|
|
24
|
+
const Root = app.getRootComponent();
|
|
25
|
+
render(<Root />);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const waitForGetRequests = async (app: MockClientApplication, urls: string[]) => {
|
|
29
|
+
await waitFor(
|
|
30
|
+
() => {
|
|
31
|
+
const history = app.apiMock.history.get.map((request) => request.url);
|
|
32
|
+
expect(history).toEqual(expect.arrayContaining(urls));
|
|
33
|
+
},
|
|
34
|
+
{ timeout: 3000 },
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const setupApp = (pmList: any[]) => {
|
|
39
|
+
const app = createMockClient({
|
|
40
|
+
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
41
|
+
router: { type: 'memory', initialEntries: ['/admin/settings/plugin-manager'] },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.apiMock.onGet('/auth:check').reply(200, {
|
|
45
|
+
data: { id: 1, nickname: 'Super Admin' },
|
|
46
|
+
});
|
|
47
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
48
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
49
|
+
});
|
|
50
|
+
app.apiMock.onGet('app:getInfo').reply(200, { data: { id: 'mock-app' } });
|
|
51
|
+
app.apiMock.onGet('roles:check').reply(200, {
|
|
52
|
+
data: { role: 'root', snippets: ['pm', 'pm.system-settings.system-settings'] },
|
|
53
|
+
});
|
|
54
|
+
app.apiMock.onGet('/desktopRoutes:listAccessible').reply(200, { data: [] });
|
|
55
|
+
app.apiMock.onGet('systemSettings:get').reply(200, {
|
|
56
|
+
data: {
|
|
57
|
+
id: 1,
|
|
58
|
+
title: 'NocoBase',
|
|
59
|
+
raw_title: 'NocoBase',
|
|
60
|
+
enabledLanguages: ['en-US'],
|
|
61
|
+
logo: null,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
app.apiMock.onGet('pm:list').reply(200, { data: pmList });
|
|
65
|
+
app.apiMock.onGet('pm:listEnabledV2').reply(200, { data: [] });
|
|
66
|
+
|
|
67
|
+
// pm:* mutations default to GET in axios when called via api.request without method
|
|
68
|
+
app.apiMock.onGet('pm:enable').reply(200, { data: {} });
|
|
69
|
+
app.apiMock.onGet('pm:disable').reply(200, { data: {} });
|
|
70
|
+
app.apiMock.onGet('pm:remove').reply(200, { data: {} });
|
|
71
|
+
|
|
72
|
+
return app;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe('plugin-manager page', () => {
|
|
76
|
+
it('fires pm:enable when toggling switch on a disabled plugin', async () => {
|
|
77
|
+
const app = setupApp([
|
|
78
|
+
{
|
|
79
|
+
name: 'demo-plugin',
|
|
80
|
+
packageName: '@nocobase/demo-plugin',
|
|
81
|
+
displayName: 'Demo plugin',
|
|
82
|
+
description: 'A demo',
|
|
83
|
+
enabled: false,
|
|
84
|
+
builtIn: false,
|
|
85
|
+
removable: true,
|
|
86
|
+
version: '0.1.0',
|
|
87
|
+
isCompatible: true,
|
|
88
|
+
keywords: [],
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
renderApp(app);
|
|
93
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
|
|
94
|
+
|
|
95
|
+
const card = await screen.findByRole('button', { name: 'Demo plugin' });
|
|
96
|
+
const switchControl = within(card.closest('.ant-card') as HTMLElement).getByRole('switch');
|
|
97
|
+
expect(switchControl).toHaveAttribute('aria-checked', 'false');
|
|
98
|
+
|
|
99
|
+
fireEvent.click(switchControl);
|
|
100
|
+
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
const enableCall = app.apiMock.history.get.find((req) => req.url === 'pm:enable');
|
|
103
|
+
expect(enableCall).toBeDefined();
|
|
104
|
+
expect(enableCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('fires pm:disable after confirm when toggling switch on an enabled plugin', async () => {
|
|
109
|
+
const app = setupApp([
|
|
110
|
+
{
|
|
111
|
+
name: 'demo-plugin',
|
|
112
|
+
packageName: '@nocobase/demo-plugin',
|
|
113
|
+
displayName: 'Demo plugin',
|
|
114
|
+
description: 'A demo',
|
|
115
|
+
enabled: true,
|
|
116
|
+
builtIn: false,
|
|
117
|
+
removable: true,
|
|
118
|
+
version: '0.1.0',
|
|
119
|
+
isCompatible: true,
|
|
120
|
+
keywords: [],
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
renderApp(app);
|
|
125
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
|
|
126
|
+
|
|
127
|
+
const card = await screen.findByRole('button', { name: 'Demo plugin' });
|
|
128
|
+
const switchControl = within(card.closest('.ant-card') as HTMLElement).getByRole('switch');
|
|
129
|
+
expect(switchControl).toHaveAttribute('aria-checked', 'true');
|
|
130
|
+
|
|
131
|
+
fireEvent.click(switchControl);
|
|
132
|
+
|
|
133
|
+
const confirmTitle = await screen.findByText('Are you sure to disable this plugin?');
|
|
134
|
+
const confirmDialog = confirmTitle.closest('.ant-modal-confirm') as HTMLElement;
|
|
135
|
+
expect(confirmDialog).not.toBeNull();
|
|
136
|
+
const okButton = within(confirmDialog).getByText('OK');
|
|
137
|
+
fireEvent.click(okButton);
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
const disableCall = app.apiMock.history.get.find((req) => req.url === 'pm:disable');
|
|
141
|
+
expect(disableCall).toBeDefined();
|
|
142
|
+
expect(disableCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('fires pm:remove after Popconfirm on a removable plugin', async () => {
|
|
147
|
+
const app = setupApp([
|
|
148
|
+
{
|
|
149
|
+
name: 'demo-plugin',
|
|
150
|
+
packageName: '@nocobase/demo-plugin',
|
|
151
|
+
displayName: 'Demo plugin',
|
|
152
|
+
description: 'A demo',
|
|
153
|
+
enabled: false,
|
|
154
|
+
builtIn: false,
|
|
155
|
+
removable: true,
|
|
156
|
+
version: '0.1.0',
|
|
157
|
+
isCompatible: true,
|
|
158
|
+
keywords: [],
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
renderApp(app);
|
|
163
|
+
await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
|
|
164
|
+
|
|
165
|
+
const removeLink = await screen.findByText('Remove');
|
|
166
|
+
fireEvent.click(removeLink);
|
|
167
|
+
|
|
168
|
+
const yesButton = await screen.findByRole('button', { name: 'Yes' });
|
|
169
|
+
fireEvent.click(yesButton);
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
const removeCall = app.apiMock.history.get.find((req) => req.url === 'pm:remove');
|
|
173
|
+
expect(removeCall).toBeDefined();
|
|
174
|
+
expect(removeCall?.params).toMatchObject({ filterByTk: 'demo-plugin' });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -14,6 +14,7 @@ import { message } from 'antd';
|
|
|
14
14
|
import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
|
|
15
15
|
import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
|
|
16
16
|
import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
|
|
17
|
+
import { matchSettingsRoute } from '../settings-center/utils';
|
|
17
18
|
|
|
18
19
|
class TestAclPlugin extends Plugin {
|
|
19
20
|
async load() {
|
|
@@ -145,6 +146,28 @@ const mockAdminRuntime = (
|
|
|
145
146
|
};
|
|
146
147
|
|
|
147
148
|
describe('settings center', () => {
|
|
149
|
+
it('should match nested layout paths under a registered settings page', () => {
|
|
150
|
+
const settings = {
|
|
151
|
+
'/admin/settings/public-forms': {
|
|
152
|
+
name: 'public-forms.index',
|
|
153
|
+
topLevelName: 'public-forms',
|
|
154
|
+
path: '/admin/settings/public-forms',
|
|
155
|
+
},
|
|
156
|
+
'/admin/settings/public-forms/advanced': {
|
|
157
|
+
name: 'public-forms.advanced',
|
|
158
|
+
topLevelName: 'public-forms',
|
|
159
|
+
path: '/admin/settings/public-forms/advanced',
|
|
160
|
+
},
|
|
161
|
+
} as any;
|
|
162
|
+
|
|
163
|
+
expect(matchSettingsRoute(settings, '/admin/settings/public-forms/form-1')).toMatchObject({
|
|
164
|
+
name: 'public-forms.index',
|
|
165
|
+
});
|
|
166
|
+
expect(matchSettingsRoute(settings, '/admin/settings/public-forms/advanced/form-1')).toMatchObject({
|
|
167
|
+
name: 'public-forms.advanced',
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
148
171
|
it('should redirect /admin/settings to system-settings by default', async () => {
|
|
149
172
|
const app = createMockClient({
|
|
150
173
|
plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
|
|
@@ -211,8 +234,7 @@ describe('settings center', () => {
|
|
|
211
234
|
await renderApp(app);
|
|
212
235
|
await waitForGetRequests(app, ['/auth:check', 'roles:check', 'pm:list']);
|
|
213
236
|
|
|
214
|
-
expect(await screen.findByText('
|
|
215
|
-
expect(screen.getByText('@nocobase/demo-plugin')).toBeInTheDocument();
|
|
237
|
+
expect(await screen.findByText('Demo plugin')).toBeInTheDocument();
|
|
216
238
|
});
|
|
217
239
|
|
|
218
240
|
it('should hide plugin-manager menu item when pm snippet is missing', async () => {
|