@nocobase/client-v2 2.1.0-beta.34 → 2.1.0-beta.36
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/BaseApplication.d.ts +7 -1
- package/es/PluginManager.d.ts +2 -0
- package/es/components/PoweredBy.d.ts +18 -0
- package/es/components/SwitchLanguage.d.ts +11 -0
- package/es/components/form/DialogFormLayout.d.ts +75 -0
- package/es/components/form/DrawerFormLayout.d.ts +11 -11
- package/es/components/form/PasswordInput.d.ts +40 -0
- package/es/components/form/RemoteSelect.d.ts +79 -0
- package/es/components/form/index.d.ts +3 -0
- package/es/components/form/table/styles.d.ts +10 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/models/base/ActionModelCore.d.ts +6 -0
- package/es/flow/models/base/GridModel.d.ts +2 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
- package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/hooks/index.d.ts +2 -0
- package/es/hooks/useCurrentAppInfo.d.ts +9 -0
- package/es/index.mjs +117 -105
- package/es/json-logic/globalOperators.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +25 -0
- package/es/utils/appVersionHTML.d.ts +10 -0
- package/es/utils/globalDeps.d.ts +7 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/remotePlugins.d.ts +4 -1
- package/lib/index.js +120 -108
- package/package.json +7 -6
- package/src/BaseApplication.tsx +11 -3
- package/src/PluginManager.ts +2 -0
- package/src/PluginSettingsManager.ts +2 -1
- package/src/__tests__/PluginSettingsManager.test.ts +19 -0
- package/src/__tests__/PoweredBy.test.tsx +130 -0
- package/src/__tests__/app.test.tsx +39 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
- package/src/__tests__/remotePlugins.test.ts +203 -0
- package/src/__tests__/useCurrentRoles.test.tsx +100 -0
- package/src/components/PoweredBy.tsx +71 -0
- package/src/components/README.md +314 -0
- package/src/components/README.zh-CN.md +312 -0
- package/src/components/SwitchLanguage.tsx +48 -0
- package/src/components/form/DialogFormLayout.tsx +111 -0
- package/src/components/form/DrawerFormLayout.tsx +13 -32
- package/src/components/form/PasswordInput.tsx +211 -0
- package/src/components/form/RemoteSelect.tsx +137 -0
- package/src/components/form/index.tsx +3 -0
- package/src/components/form/table/Table.tsx +2 -1
- package/src/components/form/table/styles.ts +19 -0
- package/src/components/index.ts +2 -0
- package/src/css-variable/CSSVariableProvider.tsx +10 -1
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
- package/src/flow/actions/dataScope.tsx +3 -0
- package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/models/base/ActionModel.tsx +8 -7
- package/src/flow/models/base/ActionModelCore.tsx +15 -7
- package/src/flow/models/base/GridModel.tsx +93 -36
- package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
- package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useCurrentAppInfo.ts +36 -0
- package/src/json-logic/globalOperators.js +731 -0
- package/src/nocobase-buildin-plugin/index.tsx +70 -16
- package/src/utils/appVersionHTML.ts +28 -0
- package/src/utils/globalDeps.ts +47 -31
- package/src/utils/index.tsx +2 -0
- package/src/utils/remotePlugins.ts +119 -13
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.36",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
"@formily/antd-v5": "1.2.3",
|
|
27
27
|
"@formily/react": "^2.2.27",
|
|
28
28
|
"@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.
|
|
29
|
+
"@nocobase/evaluators": "2.1.0-beta.36",
|
|
30
|
+
"@nocobase/flow-engine": "2.1.0-beta.36",
|
|
31
|
+
"@nocobase/sdk": "2.1.0-beta.36",
|
|
32
|
+
"@nocobase/shared": "2.1.0-beta.36",
|
|
33
|
+
"@nocobase/utils": "2.1.0-beta.36",
|
|
33
34
|
"ahooks": "^3.7.2",
|
|
34
35
|
"antd": "5.24.2",
|
|
35
36
|
"antd-style": "3.7.1",
|
|
@@ -43,5 +44,5 @@
|
|
|
43
44
|
"react-i18next": "^11.15.1",
|
|
44
45
|
"react-router-dom": "^6.30.1"
|
|
45
46
|
},
|
|
46
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "397d45c744f6eb48b3a0cd785c87cbf1257c3513"
|
|
47
48
|
}
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
RouterOptions,
|
|
41
41
|
} from './RouterManager';
|
|
42
42
|
import { WebSocketClient, type WebSocketClientOptions } from './WebSocketClient';
|
|
43
|
+
import { getOperators } from './json-logic/globalOperators';
|
|
43
44
|
import { compose, normalizeContainer } from './utils';
|
|
44
45
|
import { defineGlobalDeps } from './utils/globalDeps';
|
|
45
46
|
import { getRequireJs } from './utils/requirejs';
|
|
@@ -57,6 +58,11 @@ type AuthTokenPayload = {
|
|
|
57
58
|
token: string;
|
|
58
59
|
authenticator: string | null;
|
|
59
60
|
};
|
|
61
|
+
export type JsonLogic = {
|
|
62
|
+
apply: (logic: any, data?: any) => any;
|
|
63
|
+
addOperation: (name: string, fn?: any) => void;
|
|
64
|
+
rmOperation: (name: string) => void;
|
|
65
|
+
};
|
|
60
66
|
|
|
61
67
|
const LEADING_SLASHES_REGEXP = /^\/+/;
|
|
62
68
|
const TRAILING_SLASHES_REGEXP = /\/+$/;
|
|
@@ -123,6 +129,7 @@ export abstract class BaseApplication<
|
|
|
123
129
|
public favicon!: string;
|
|
124
130
|
public flowEngine: FlowEngine;
|
|
125
131
|
public dataSourceManager: any;
|
|
132
|
+
public jsonLogic!: JsonLogic;
|
|
126
133
|
public context: FlowEngineContext & {
|
|
127
134
|
routeRepository: RouteRepository;
|
|
128
135
|
appInfo: Promise<Record<string, any>>;
|
|
@@ -220,6 +227,7 @@ export abstract class BaseApplication<
|
|
|
220
227
|
|
|
221
228
|
protected afterManagersInitialized() {
|
|
222
229
|
this.aiManager = new AIManager(this);
|
|
230
|
+
this.jsonLogic = getOperators();
|
|
223
231
|
}
|
|
224
232
|
|
|
225
233
|
protected configureContext() {
|
|
@@ -360,11 +368,11 @@ export abstract class BaseApplication<
|
|
|
360
368
|
});
|
|
361
369
|
}
|
|
362
370
|
|
|
363
|
-
updateFavicon(favicon?: string) {
|
|
371
|
+
updateFavicon(favicon?: string | null) {
|
|
364
372
|
let faviconLinkElement = document.querySelector('link[rel="shortcut icon"]') as HTMLLinkElement;
|
|
365
373
|
|
|
366
|
-
if (
|
|
367
|
-
this.favicon = favicon;
|
|
374
|
+
if (arguments.length > 0) {
|
|
375
|
+
this.favicon = favicon || '';
|
|
368
376
|
}
|
|
369
377
|
|
|
370
378
|
const iconHref = this.favicon || '/favicon/favicon.ico';
|
package/src/PluginManager.ts
CHANGED
|
@@ -432,7 +432,7 @@ export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseAppli
|
|
|
432
432
|
return null;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
-
const { title, aclSnippet, key, menuKey, name, ...others } = page;
|
|
435
|
+
const { title, aclSnippet, key, menuKey, name, icon, ...others } = page;
|
|
436
436
|
|
|
437
437
|
return {
|
|
438
438
|
...others,
|
|
@@ -443,6 +443,7 @@ export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseAppli
|
|
|
443
443
|
name,
|
|
444
444
|
title,
|
|
445
445
|
label: title,
|
|
446
|
+
icon: this.renderIcon(icon),
|
|
446
447
|
path: this.getRoutePath(name),
|
|
447
448
|
sort: page.sort,
|
|
448
449
|
isAllow,
|
|
@@ -76,6 +76,25 @@ describe('PluginSettingsManager v2', () => {
|
|
|
76
76
|
expect(app.router.get('admin.settings.demo.advanced')).toMatchObject({ path: 'advanced' });
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
it('should render string icon on both menu and page tab via renderIcon', () => {
|
|
80
|
+
// Previously `renderPage` spread the raw `icon` string straight to the antd
|
|
81
|
+
// Menu item, which displayed "LockOutlinedTitle" as text. Both `renderMenuItem`
|
|
82
|
+
// and `renderPage` must coerce string icon names to React elements.
|
|
83
|
+
const app = createMockClient();
|
|
84
|
+
|
|
85
|
+
app.pluginSettingsManager.addMenuItem({ key: 'demo', title: 'Demo', icon: 'TeamOutlined' });
|
|
86
|
+
app.pluginSettingsManager.addPageTabItem({
|
|
87
|
+
menuKey: 'demo',
|
|
88
|
+
key: 'index',
|
|
89
|
+
title: 'Overview',
|
|
90
|
+
icon: 'LockOutlined',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const list = app.pluginSettingsManager.getList();
|
|
94
|
+
expect(React.isValidElement(list[0].icon)).toBe(true);
|
|
95
|
+
expect(React.isValidElement(list[0].children?.[0].icon)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
79
98
|
it('should support componentLoader on page item', () => {
|
|
80
99
|
const app = createMockClient();
|
|
81
100
|
const componentLoader = async () => ({
|
|
@@ -0,0 +1,130 @@
|
|
|
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 { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { createMockClient } from '../MockApplication';
|
|
13
|
+
import { Plugin } from '../Plugin';
|
|
14
|
+
import PoweredBy from '../components/PoweredBy';
|
|
15
|
+
|
|
16
|
+
class PoweredByRoutePlugin extends Plugin {
|
|
17
|
+
async load() {
|
|
18
|
+
this.router.add('root', {
|
|
19
|
+
path: '/',
|
|
20
|
+
Component: PoweredBy,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class MockCustomBrandPlugin extends Plugin {}
|
|
26
|
+
|
|
27
|
+
const renderPoweredBy = async (plugins: any[] = [], appInfoData: Record<string, any> = { version: '1.2.3' }) => {
|
|
28
|
+
const app = createMockClient({
|
|
29
|
+
plugins: [PoweredByRoutePlugin as any, ...plugins],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.apiMock.onGet('app:getInfo').reply(200, {
|
|
33
|
+
data: appInfoData,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const Root = app.getRootComponent();
|
|
37
|
+
const result = render(<Root />);
|
|
38
|
+
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(document.querySelector('.ant-spin-spinning')).not.toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe('PoweredBy', () => {
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should render the default brand when custom-brand is not installed', async () => {
|
|
52
|
+
const { container } = await renderPoweredBy();
|
|
53
|
+
|
|
54
|
+
expect(screen.getByRole('link', { name: 'NocoBase' })).toHaveAttribute('href', 'https://www.nocobase.com');
|
|
55
|
+
expect(container).toHaveTextContent('Powered by NocoBase');
|
|
56
|
+
// The `.nb-brand` className is reserved for the custom-brand HTML branch
|
|
57
|
+
// so downstream stylesheets can selectively target customised content
|
|
58
|
+
// without leaking onto the default footer.
|
|
59
|
+
expect(container.querySelector('.nb-brand')).not.toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should render custom-brand HTML and replace appVersion', async () => {
|
|
63
|
+
const { container } = await renderPoweredBy([
|
|
64
|
+
[
|
|
65
|
+
MockCustomBrandPlugin,
|
|
66
|
+
{
|
|
67
|
+
packageName: '@nocobase/plugin-custom-brand',
|
|
68
|
+
options: {
|
|
69
|
+
brand: '<span>Custom Brand</span>{{appVersion}}',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
await waitFor(() => {
|
|
76
|
+
expect(container.querySelector('.nb-brand')).toHaveTextContent('Custom Brandv1.2.3');
|
|
77
|
+
});
|
|
78
|
+
expect(container.querySelector('.nb-app-version')).toHaveTextContent('v1.2.3');
|
|
79
|
+
expect(screen.queryByText('Powered by')).not.toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should not render undefined appVersion when app version is unavailable', async () => {
|
|
83
|
+
const { container } = await renderPoweredBy(
|
|
84
|
+
[
|
|
85
|
+
[
|
|
86
|
+
MockCustomBrandPlugin,
|
|
87
|
+
{
|
|
88
|
+
packageName: '@nocobase/plugin-custom-brand',
|
|
89
|
+
options: {
|
|
90
|
+
brand: '<span>Custom Brand</span>{{appVersion}}',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
],
|
|
95
|
+
{},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(container.querySelector('.nb-brand')).toHaveTextContent('Custom Brand');
|
|
99
|
+
expect(container.querySelector('.nb-brand')).not.toHaveTextContent('undefined');
|
|
100
|
+
expect(container.querySelector('.nb-app-version')).not.toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should escape custom-brand appVersion placeholder', async () => {
|
|
104
|
+
// Defence in depth: even if the back-end ever returns a tampered
|
|
105
|
+
// `app:getInfo` payload, the version string must be HTML-escaped
|
|
106
|
+
// before being interpolated into the custom-brand template — never
|
|
107
|
+
// produce a live `<script>` node in the DOM.
|
|
108
|
+
const { container } = await renderPoweredBy(
|
|
109
|
+
[
|
|
110
|
+
[
|
|
111
|
+
MockCustomBrandPlugin,
|
|
112
|
+
{
|
|
113
|
+
packageName: '@nocobase/plugin-custom-brand',
|
|
114
|
+
options: {
|
|
115
|
+
brand: '<span>Custom Brand</span>{{appVersion}}',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
],
|
|
120
|
+
{
|
|
121
|
+
version: '<script>alert(1)</script>&"',
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(container.querySelector('.nb-app-version')).toHaveTextContent('v<script>alert(1)</script>&"');
|
|
127
|
+
});
|
|
128
|
+
expect(container.querySelector('.nb-brand script')).not.toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -12,6 +12,7 @@ import { useFlowEngineContext } from '@nocobase/flow-engine';
|
|
|
12
12
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { Outlet } from 'react-router-dom';
|
|
15
|
+
import { escapeHTML, getAppVersionHTML } from '../utils';
|
|
15
16
|
|
|
16
17
|
const waitForAppReady = async () => {
|
|
17
18
|
await waitFor(() => {
|
|
@@ -48,6 +49,14 @@ describe('app', () => {
|
|
|
48
49
|
expect(app.getHref('/test')).toBe('/test');
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
it('should initialize shared jsonLogic operators', () => {
|
|
53
|
+
const app = new Application({ router });
|
|
54
|
+
|
|
55
|
+
expect(app.jsonLogic.apply({ $eq: [1, '1'] })).toBe(true);
|
|
56
|
+
app.jsonLogic.addOperation('$testAlwaysTrue', () => true);
|
|
57
|
+
expect(app.jsonLogic.apply({ $testAlwaysTrue: [] })).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
51
60
|
it('should apply the provided favicon immediately', () => {
|
|
52
61
|
const app = new Application({ router });
|
|
53
62
|
|
|
@@ -58,6 +67,36 @@ describe('app', () => {
|
|
|
58
67
|
expect(favicon.getAttribute('href')).toBe('/custom-favicon.ico');
|
|
59
68
|
});
|
|
60
69
|
|
|
70
|
+
it('should reset favicon to default when favicon is cleared', () => {
|
|
71
|
+
const app = new Application({ router });
|
|
72
|
+
|
|
73
|
+
app.updateFavicon('/custom-favicon.ico');
|
|
74
|
+
app.updateFavicon(null);
|
|
75
|
+
|
|
76
|
+
const favicon = document.querySelector('link[rel="shortcut icon"]') as HTMLLinkElement;
|
|
77
|
+
expect(favicon).toBeInTheDocument();
|
|
78
|
+
expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reset favicon to default when favicon is explicitly undefined', () => {
|
|
82
|
+
const app = new Application({ router });
|
|
83
|
+
|
|
84
|
+
app.updateFavicon('/custom-favicon.ico');
|
|
85
|
+
app.updateFavicon(undefined);
|
|
86
|
+
|
|
87
|
+
const favicon = document.querySelector('link[rel="shortcut icon"]') as HTMLLinkElement;
|
|
88
|
+
expect(favicon).toBeInTheDocument();
|
|
89
|
+
expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should escape app version html placeholder content', () => {
|
|
93
|
+
expect(getAppVersionHTML('<script>alert(1)</script>&"')).toBe(
|
|
94
|
+
'<span class="nb-app-version">v<script>alert(1)</script>&"</span>',
|
|
95
|
+
);
|
|
96
|
+
expect(getAppVersionHTML(undefined)).toBe('');
|
|
97
|
+
expect(escapeHTML("NocoBase <v2> & 'beta'")).toBe('NocoBase <v2> & 'beta'');
|
|
98
|
+
});
|
|
99
|
+
|
|
61
100
|
it('should reject invalid component objects but keep valid exotic components', () => {
|
|
62
101
|
const app = new Application({ router });
|
|
63
102
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
@@ -39,30 +39,18 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
39
39
|
vi.restoreAllMocks();
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('should
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
...originalLocation,
|
|
48
|
-
pathname: '/v2/admin/7vu4c2sdk6h',
|
|
49
|
-
search: '',
|
|
50
|
-
hash: '',
|
|
51
|
-
replace,
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
|
|
42
|
+
it('should navigate to v2 signin when /auth:check returns no user', async () => {
|
|
43
|
+
// Aligns with v1: use react-router navigate (virtual) rather than
|
|
44
|
+
// `window.location.replace`, so a `window.location.href` queued elsewhere
|
|
45
|
+
// (e.g. 2FA's `code:302` response interceptor) can commit instead of being
|
|
46
|
+
// overridden.
|
|
55
47
|
const app = createMockClient({
|
|
56
48
|
publicPath: '/v2/',
|
|
57
49
|
plugins: [NocoBaseBuildInPlugin as any],
|
|
58
50
|
router: { type: 'memory', initialEntries: ['/v2/admin/7vu4c2sdk6h'] },
|
|
59
51
|
});
|
|
60
52
|
app.apiMock.onGet('app:getLang').reply(200, {
|
|
61
|
-
data: {
|
|
62
|
-
lang: 'en-US',
|
|
63
|
-
resources: { client: {} },
|
|
64
|
-
cron: {},
|
|
65
|
-
},
|
|
53
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
66
54
|
});
|
|
67
55
|
app.apiMock.onGet('/auth:check').reply(200, { data: {} });
|
|
68
56
|
|
|
@@ -70,68 +58,63 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
70
58
|
render(<Root />);
|
|
71
59
|
|
|
72
60
|
await waitFor(() => {
|
|
73
|
-
expect(
|
|
61
|
+
expect(app.router.router.state.location.pathname).toBe('/v2/signin');
|
|
62
|
+
expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
74
63
|
});
|
|
75
64
|
});
|
|
76
65
|
|
|
77
|
-
it('should
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
},
|
|
66
|
+
it('should short-circuit /auth:check when server returns code:302 instead of redirecting to signin', async () => {
|
|
67
|
+
// When the server signals an intermediate redirect (typically 2FA verify),
|
|
68
|
+
// CurrentUserProvider must NOT treat the missing `user.id` as "logged out"
|
|
69
|
+
// and race the 2FA response interceptor with its own signin redirect.
|
|
70
|
+
const app = createMockClient({
|
|
71
|
+
publicPath: '/v2/',
|
|
72
|
+
plugins: [NocoBaseBuildInPlugin as any],
|
|
73
|
+
router: { type: 'memory', initialEntries: ['/v2/admin'] },
|
|
74
|
+
});
|
|
75
|
+
app.apiMock.onGet('app:getLang').reply(200, {
|
|
76
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
77
|
+
});
|
|
78
|
+
app.apiMock.onGet('/auth:check').reply(200, {
|
|
79
|
+
data: { code: 302, redirect: '/2fa?redirect=/admin' },
|
|
88
80
|
});
|
|
89
81
|
|
|
82
|
+
const Root = app.getRootComponent();
|
|
83
|
+
render(<Root />);
|
|
84
|
+
|
|
85
|
+
// Give CurrentUserProvider time to process the response.
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
87
|
+
expect(app.router.router.state.location.pathname).toBe('/v2/admin');
|
|
88
|
+
expect(app.router.router.state.location.search).toBe('');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should redirect unauthenticated v2 root access to v2 signin via <Navigate />', async () => {
|
|
90
92
|
const app = createMockClient({
|
|
91
93
|
publicPath: '/nocobase/v2/',
|
|
92
94
|
plugins: [NocoBaseBuildInPlugin as any],
|
|
93
95
|
router: { type: 'memory', initialEntries: ['/nocobase/v2/'] },
|
|
94
96
|
});
|
|
95
97
|
app.apiMock.onGet('app:getLang').reply(200, {
|
|
96
|
-
data: {
|
|
97
|
-
lang: 'en-US',
|
|
98
|
-
resources: { client: {} },
|
|
99
|
-
cron: {},
|
|
100
|
-
},
|
|
98
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
101
99
|
});
|
|
102
100
|
|
|
103
101
|
const Root = app.getRootComponent();
|
|
104
102
|
render(<Root />);
|
|
105
103
|
|
|
106
104
|
await waitFor(() => {
|
|
107
|
-
expect(
|
|
105
|
+
expect(app.router.router.state.location.pathname).toBe('/nocobase/v2/signin');
|
|
106
|
+
expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2%2Fadmin');
|
|
108
107
|
});
|
|
109
108
|
});
|
|
110
109
|
|
|
111
110
|
it('should render v2 admin root without redirecting away', async () => {
|
|
112
|
-
const replace = vi.fn();
|
|
113
|
-
Object.defineProperty(globalThis.window, 'location', {
|
|
114
|
-
configurable: true,
|
|
115
|
-
value: {
|
|
116
|
-
...originalLocation,
|
|
117
|
-
pathname: '/v2/admin',
|
|
118
|
-
search: '',
|
|
119
|
-
hash: '',
|
|
120
|
-
replace,
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
111
|
const app = createMockClient({
|
|
125
112
|
publicPath: '/v2/',
|
|
126
113
|
plugins: [NocoBaseBuildInPlugin as any],
|
|
127
114
|
router: { type: 'memory', initialEntries: ['/v2/admin'] },
|
|
128
115
|
});
|
|
129
116
|
app.apiMock.onGet('app:getLang').reply(200, {
|
|
130
|
-
data: {
|
|
131
|
-
lang: 'en-US',
|
|
132
|
-
resources: { client: {} },
|
|
133
|
-
cron: {},
|
|
134
|
-
},
|
|
117
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
135
118
|
});
|
|
136
119
|
app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
|
|
137
120
|
app.apiMock.onGet('systemSettings:get').reply(200, { data: {} });
|
|
@@ -152,36 +135,20 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
152
135
|
await waitFor(() => {
|
|
153
136
|
expect(container.innerHTML).toContain('No pages yet, please configure first');
|
|
154
137
|
});
|
|
155
|
-
expect(
|
|
138
|
+
expect(app.router.router.state.location.pathname).toBe('/v2/admin');
|
|
156
139
|
expect(container.innerHTML).not.toContain('Legacy page');
|
|
157
140
|
});
|
|
158
141
|
|
|
159
142
|
it.each(['/v2/admin/legacy-page/tab/tab-1', '/v2/admin/legacy-page/view/detail'])(
|
|
160
143
|
'should show 404 for authenticated direct v1-style v2 page access: %s',
|
|
161
144
|
async (pathname) => {
|
|
162
|
-
const replace = vi.fn();
|
|
163
|
-
Object.defineProperty(globalThis.window, 'location', {
|
|
164
|
-
configurable: true,
|
|
165
|
-
value: {
|
|
166
|
-
...originalLocation,
|
|
167
|
-
pathname,
|
|
168
|
-
search: '?from=direct',
|
|
169
|
-
hash: '#dialog',
|
|
170
|
-
replace,
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
145
|
const app = createMockClient({
|
|
175
146
|
publicPath: '/v2/',
|
|
176
147
|
plugins: [NocoBaseBuildInPlugin as any],
|
|
177
148
|
router: { type: 'memory', initialEntries: [pathname] },
|
|
178
149
|
});
|
|
179
150
|
app.apiMock.onGet('app:getLang').reply(200, {
|
|
180
|
-
data: {
|
|
181
|
-
lang: 'en-US',
|
|
182
|
-
resources: { client: {} },
|
|
183
|
-
cron: {},
|
|
184
|
-
},
|
|
151
|
+
data: { lang: 'en-US', resources: { client: {} }, cron: {} },
|
|
185
152
|
});
|
|
186
153
|
app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
|
|
187
154
|
app.apiMock.onGet('systemSettings:get').reply(200, { data: {} });
|
|
@@ -200,7 +167,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
200
167
|
render(<Root />);
|
|
201
168
|
|
|
202
169
|
expect(await screen.findByText('404')).toBeInTheDocument();
|
|
203
|
-
expect(
|
|
170
|
+
expect(app.router.router.state.location.pathname).toBe(pathname);
|
|
204
171
|
},
|
|
205
172
|
);
|
|
206
173
|
});
|