@nocobase/client-v2 2.1.0-beta.33 → 2.1.0-beta.34
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/APIClient.d.ts +16 -0
- package/es/Application.d.ts +2 -1
- package/es/authRedirect.d.ts +9 -16
- package/es/components/form/EnvVariableInput.d.ts +8 -6
- package/es/components/form/VariableInput.d.ts +73 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
- package/es/components/form/table/SelectionCell.d.ts +36 -0
- package/es/components/form/table/Table.d.ts +82 -0
- package/es/components/form/table/constants.d.ts +15 -0
- package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
- package/es/components/form/table/dnd/index.d.ts +9 -0
- package/es/components/form/table/index.d.ts +9 -0
- package/es/components/form/table/styles.d.ts +41 -0
- package/es/components/form/table/utils.d.ts +44 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
- package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +145 -78
- package/es/theme/globalStyles.d.ts +9 -0
- package/es/theme/index.d.ts +1 -0
- package/lib/index.js +161 -94
- package/package.json +8 -6
- package/src/APIClient.ts +68 -0
- package/src/Application.tsx +6 -2
- package/src/__tests__/authRedirect.test.ts +170 -64
- package/src/__tests__/globalDeps.test.ts +2 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
- package/src/authRedirect.ts +23 -84
- package/src/components/form/EnvVariableInput.tsx +11 -46
- package/src/components/form/VariableInput.tsx +177 -0
- package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/form/table/RowOverlayPreview.tsx +51 -0
- package/src/components/form/table/SelectionCell.tsx +72 -0
- package/src/components/form/table/Table.tsx +279 -0
- package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
- package/src/components/form/table/constants.ts +16 -0
- package/src/components/form/table/dnd/SortableRow.tsx +106 -0
- package/src/components/form/table/dnd/index.ts +10 -0
- package/src/components/form/table/index.tsx +13 -0
- package/src/components/form/table/styles.ts +110 -0
- package/src/components/form/table/utils.ts +75 -0
- package/src/components/index.ts +2 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
- package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
- package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
- package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
- package/src/index.ts +1 -0
- package/src/nocobase-buildin-plugin/index.tsx +4 -4
- package/src/theme/globalStyles.ts +21 -0
- package/src/theme/index.tsx +1 -0
- package/src/utils/globalDeps.ts +5 -1
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.34",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -20,14 +20,16 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@ant-design/icons": "^5.6.1",
|
|
23
|
+
"@dnd-kit/core": "^6.0.0",
|
|
24
|
+
"@dnd-kit/sortable": "^7.0.0",
|
|
23
25
|
"@emotion/css": "^11.7.1",
|
|
24
26
|
"@formily/antd-v5": "1.2.3",
|
|
25
27
|
"@formily/react": "^2.2.27",
|
|
26
28
|
"@formily/shared": "^2.2.27",
|
|
27
|
-
"@nocobase/evaluators": "2.1.0-beta.
|
|
28
|
-
"@nocobase/flow-engine": "2.1.0-beta.
|
|
29
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
30
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
29
|
+
"@nocobase/evaluators": "2.1.0-beta.34",
|
|
30
|
+
"@nocobase/flow-engine": "2.1.0-beta.34",
|
|
31
|
+
"@nocobase/sdk": "2.1.0-beta.34",
|
|
32
|
+
"@nocobase/shared": "2.1.0-beta.34",
|
|
31
33
|
"ahooks": "^3.7.2",
|
|
32
34
|
"antd": "5.24.2",
|
|
33
35
|
"antd-style": "3.7.1",
|
|
@@ -41,5 +43,5 @@
|
|
|
41
43
|
"react-i18next": "^11.15.1",
|
|
42
44
|
"react-router-dom": "^6.30.1"
|
|
43
45
|
},
|
|
44
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "ca804833299c547f8d49f8d58f73273a4bfcd03c"
|
|
45
47
|
}
|
package/src/APIClient.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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 { APIClient as APIClientSDK, type APIClientOptions, hasHeaderValue } from '@nocobase/sdk';
|
|
11
|
+
|
|
12
|
+
function offsetToTimeZone(offset: number) {
|
|
13
|
+
const hours = Math.floor(Math.abs(offset));
|
|
14
|
+
const minutes = Math.abs((offset % 1) * 60);
|
|
15
|
+
const formattedHours = String(hours).padStart(2, '0');
|
|
16
|
+
const formattedMinutes = String(minutes).padStart(2, '0');
|
|
17
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
18
|
+
return `${sign}${formattedHours}:${formattedMinutes}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getCurrentTimezone() {
|
|
22
|
+
return offsetToTimeZone(new Date().getTimezoneOffset() / -60);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class APIClient extends APIClientSDK {
|
|
26
|
+
appName?: string;
|
|
27
|
+
|
|
28
|
+
constructor(options?: APIClientOptions) {
|
|
29
|
+
super(options);
|
|
30
|
+
if (options && typeof options !== 'function') {
|
|
31
|
+
this.appName = options.appName;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getHostname() {
|
|
36
|
+
if (process.env.API_BASE_URL) {
|
|
37
|
+
try {
|
|
38
|
+
return new URL(process.env.API_BASE_URL).hostname;
|
|
39
|
+
} catch {
|
|
40
|
+
// fall through to window.location.hostname
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return window?.location?.hostname;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getHeaders() {
|
|
47
|
+
const headers = super.getHeaders();
|
|
48
|
+
if (this.appName) {
|
|
49
|
+
headers['X-App'] = this.appName;
|
|
50
|
+
}
|
|
51
|
+
headers['X-Timezone'] = getCurrentTimezone();
|
|
52
|
+
headers['X-Hostname'] = this.getHostname();
|
|
53
|
+
return headers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interceptors() {
|
|
57
|
+
this.axios.interceptors.request.use((config) => {
|
|
58
|
+
const headers = this.getHeaders();
|
|
59
|
+
Object.keys(headers).forEach((key) => {
|
|
60
|
+
if (!hasHeaderValue(config.headers, key)) {
|
|
61
|
+
config.headers[key] = headers[key];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return config;
|
|
65
|
+
});
|
|
66
|
+
super.interceptors();
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/Application.tsx
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { type APIClientOptions, getSubAppName } from '@nocobase/sdk';
|
|
11
11
|
import { createInstance, type i18n as i18next } from 'i18next';
|
|
12
12
|
import { initReactI18next } from 'react-i18next';
|
|
13
13
|
|
|
14
|
+
import { APIClient } from './APIClient';
|
|
14
15
|
import { BaseApplication, type BaseApplicationOptions } from './BaseApplication';
|
|
15
16
|
import { CollectionFieldInterfaceManager } from './collection-field-interface/CollectionFieldInterfaceManager';
|
|
16
17
|
import type { PluginType } from './PluginManager';
|
|
@@ -43,7 +44,10 @@ export class Application extends BaseApplication<
|
|
|
43
44
|
public declare dataSourceManager: any;
|
|
44
45
|
|
|
45
46
|
protected createApiClient(options: ApplicationOptions) {
|
|
46
|
-
return new APIClient(
|
|
47
|
+
return new APIClient({
|
|
48
|
+
...options.apiClient,
|
|
49
|
+
appName: options.name || getSubAppName(options.publicPath),
|
|
50
|
+
});
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
protected configureRuntimeAdapters() {
|
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
convertV2AdminPathToLegacy,
|
|
11
|
+
buildV2SigninHref,
|
|
13
12
|
getCurrentV2RedirectPath,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
redirectToV2Signin,
|
|
14
|
+
resolveV2SigninRedirect,
|
|
16
15
|
} from '../authRedirect';
|
|
17
16
|
|
|
18
17
|
describe('auth redirect helpers', () => {
|
|
@@ -60,7 +59,7 @@ describe('auth redirect helpers', () => {
|
|
|
60
59
|
).toBe('/nocobase/v2/admin/7vu4c2sdk6h?from=menu#tab-1');
|
|
61
60
|
});
|
|
62
61
|
|
|
63
|
-
it('should derive
|
|
62
|
+
it('should derive v2 signin href from v2 public path', () => {
|
|
64
63
|
const app = {
|
|
65
64
|
getPublicPath: () => '/nocobase/v2/',
|
|
66
65
|
router: {
|
|
@@ -68,12 +67,12 @@ describe('auth redirect helpers', () => {
|
|
|
68
67
|
},
|
|
69
68
|
} as any;
|
|
70
69
|
|
|
71
|
-
expect(
|
|
72
|
-
'/nocobase/signin?redirect=%2Fnocobase%2Fv2%2Fadmin%2F7vu4c2sdk6h',
|
|
70
|
+
expect(buildV2SigninHref(app, '/nocobase/v2/admin/7vu4c2sdk6h')).toBe(
|
|
71
|
+
'/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin%2F7vu4c2sdk6h',
|
|
73
72
|
);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
|
-
it('should
|
|
75
|
+
it('should derive v2 signin href when root public path is "/"', () => {
|
|
77
76
|
const app = {
|
|
78
77
|
getPublicPath: () => '/v2/',
|
|
79
78
|
router: {
|
|
@@ -81,53 +80,7 @@ describe('auth redirect helpers', () => {
|
|
|
81
80
|
},
|
|
82
81
|
} as any;
|
|
83
82
|
|
|
84
|
-
expect(
|
|
85
|
-
expect(convertV2AdminPathToLegacy(app, '/v2/admin/page-1/tab/tab-1')).toBe('/admin/page-1/tabs/tab-1');
|
|
86
|
-
expect(convertV2AdminPathToLegacy(app, '/v2/admin/page-1/view/detail')).toBe('/admin/page-1/popups/detail');
|
|
87
|
-
expect(convertV2AdminPathToLegacy(app, '/v2/admin/page-1/tab/tab-1/view/detail')).toBe(
|
|
88
|
-
'/admin/page-1/tabs/tab-1/popups/detail',
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should preserve basename search and hash when converting current legacy path', () => {
|
|
93
|
-
const app = {
|
|
94
|
-
getPublicPath: () => '/nocobase/v2/',
|
|
95
|
-
router: {
|
|
96
|
-
getBasename: () => '/nocobase/v2',
|
|
97
|
-
},
|
|
98
|
-
} as any;
|
|
99
|
-
|
|
100
|
-
expect(
|
|
101
|
-
convertV2AdminPathToLegacy(app, {
|
|
102
|
-
pathname: '/nocobase/v2/admin/page-1/tab/tab-1/view/detail',
|
|
103
|
-
search: '?from=menu',
|
|
104
|
-
hash: '#dialog',
|
|
105
|
-
}),
|
|
106
|
-
).toBe('/nocobase/admin/page-1/tabs/tab-1/popups/detail?from=menu#dialog');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should normalize duplicated slashes during legacy path conversion', () => {
|
|
110
|
-
const app = {
|
|
111
|
-
getPublicPath: () => '/nocobase/v2/',
|
|
112
|
-
router: {
|
|
113
|
-
getBasename: () => '/nocobase/v2/',
|
|
114
|
-
},
|
|
115
|
-
} as any;
|
|
116
|
-
|
|
117
|
-
expect(convertV2AdminPathToLegacy(app, '/nocobase//v2//admin//page-1//tab//tab-1')).toBe(
|
|
118
|
-
'/nocobase/admin/page-1/tabs/tab-1',
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should return null for non-admin runtime paths', () => {
|
|
123
|
-
const app = {
|
|
124
|
-
getPublicPath: () => '/v2/',
|
|
125
|
-
router: {
|
|
126
|
-
getBasename: () => '/v2',
|
|
127
|
-
},
|
|
128
|
-
} as any;
|
|
129
|
-
|
|
130
|
-
expect(convertV2AdminPathToLegacy(app, '/v2/settings')).toBeNull();
|
|
83
|
+
expect(buildV2SigninHref(app, '/v2/admin/7vu4c2sdk6h')).toBe('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
131
84
|
});
|
|
132
85
|
|
|
133
86
|
it('should redirect with window.location.replace by default', () => {
|
|
@@ -147,12 +100,12 @@ describe('auth redirect helpers', () => {
|
|
|
147
100
|
},
|
|
148
101
|
} as any;
|
|
149
102
|
|
|
150
|
-
|
|
103
|
+
redirectToV2Signin(app, '/v2/admin/7vu4c2sdk6h');
|
|
151
104
|
|
|
152
|
-
expect(replace).toHaveBeenCalledWith('/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
105
|
+
expect(replace).toHaveBeenCalledWith('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
153
106
|
});
|
|
154
107
|
|
|
155
|
-
it('should accept same-origin
|
|
108
|
+
it('should accept same-origin v2 signin urls only', () => {
|
|
156
109
|
Object.defineProperty(globalThis.window, 'location', {
|
|
157
110
|
configurable: true,
|
|
158
111
|
value: {
|
|
@@ -168,14 +121,167 @@ describe('auth redirect helpers', () => {
|
|
|
168
121
|
},
|
|
169
122
|
} as any;
|
|
170
123
|
|
|
171
|
-
expect(
|
|
172
|
-
'http://localhost:20000/nocobase/signin?redirect=%2Fnocobase%2Fv2%2Fadmin#hash',
|
|
124
|
+
expect(resolveV2SigninRedirect('/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin#hash', app)).toBe(
|
|
125
|
+
'http://localhost:20000/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin#hash',
|
|
173
126
|
);
|
|
174
|
-
expect(
|
|
127
|
+
expect(resolveV2SigninRedirect('/signin?redirect=%2Fv2%2Fadmin', app)).toBe(
|
|
175
128
|
'http://localhost:20000/signin?redirect=%2Fv2%2Fadmin',
|
|
176
129
|
);
|
|
177
|
-
expect(
|
|
178
|
-
expect(
|
|
179
|
-
expect(
|
|
130
|
+
expect(resolveV2SigninRedirect('/nocobase/signin?redirect=%2Fnocobase%2Fv2%2Fadmin', app)).toBeNull();
|
|
131
|
+
expect(resolveV2SigninRedirect('https://evil.example.com/signin?redirect=%2Fv2%2Fadmin', app)).toBeNull();
|
|
132
|
+
expect(resolveV2SigninRedirect('/nocobase/admin', app)).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
|
|
136
|
+
it('should preserve sub-app segment when building current redirect path under simple public path', () => {
|
|
137
|
+
const app = {
|
|
138
|
+
getPublicPath: () => '/v2/',
|
|
139
|
+
router: {
|
|
140
|
+
getBasename: () => '/v2/apps/test-app/',
|
|
141
|
+
},
|
|
142
|
+
} as any;
|
|
143
|
+
|
|
144
|
+
expect(
|
|
145
|
+
getCurrentV2RedirectPath(app, {
|
|
146
|
+
pathname: '/v2/apps/test-app/admin/7vu4c2sdk6h',
|
|
147
|
+
search: '?tab=overview',
|
|
148
|
+
hash: '#section-a',
|
|
149
|
+
}),
|
|
150
|
+
).toBe('/v2/apps/test-app/admin/7vu4c2sdk6h?tab=overview#section-a');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should preserve sub-app segment when building current redirect path under nested public path', () => {
|
|
154
|
+
const app = {
|
|
155
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
156
|
+
router: {
|
|
157
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
158
|
+
},
|
|
159
|
+
} as any;
|
|
160
|
+
|
|
161
|
+
expect(
|
|
162
|
+
getCurrentV2RedirectPath(app, {
|
|
163
|
+
pathname: '/nocobase/v2/apps/test-app/admin/al5yj9t81of',
|
|
164
|
+
search: '?from=menu',
|
|
165
|
+
hash: '#tab-1',
|
|
166
|
+
}),
|
|
167
|
+
).toBe('/nocobase/v2/apps/test-app/admin/al5yj9t81of?from=menu#tab-1');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should derive sub-app signin href under nested public path', () => {
|
|
171
|
+
const app = {
|
|
172
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
173
|
+
router: {
|
|
174
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
175
|
+
},
|
|
176
|
+
} as any;
|
|
177
|
+
|
|
178
|
+
expect(buildV2SigninHref(app, '/nocobase/v2/apps/test-app/admin/al5yj9t81of')).toBe(
|
|
179
|
+
'/nocobase/v2/apps/test-app/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fadmin%2Fal5yj9t81of',
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should derive sub-app signin href under simple public path', () => {
|
|
184
|
+
const app = {
|
|
185
|
+
getPublicPath: () => '/v2/',
|
|
186
|
+
router: {
|
|
187
|
+
getBasename: () => '/v2/apps/test-app/',
|
|
188
|
+
},
|
|
189
|
+
} as any;
|
|
190
|
+
|
|
191
|
+
expect(buildV2SigninHref(app, '/v2/apps/test-app/admin/7vu4c2sdk6h')).toBe(
|
|
192
|
+
'/v2/apps/test-app/signin?redirect=%2Fv2%2Fapps%2Ftest-app%2Fadmin%2F7vu4c2sdk6h',
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should redirect to sub-app signin with window.location.replace', () => {
|
|
197
|
+
const replace = vi.fn();
|
|
198
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
199
|
+
configurable: true,
|
|
200
|
+
value: {
|
|
201
|
+
...originalLocation,
|
|
202
|
+
replace,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const app = {
|
|
207
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
208
|
+
router: {
|
|
209
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
210
|
+
},
|
|
211
|
+
} as any;
|
|
212
|
+
|
|
213
|
+
redirectToV2Signin(app, '/nocobase/v2/apps/test-app/admin/al5yj9t81of');
|
|
214
|
+
|
|
215
|
+
expect(replace).toHaveBeenCalledWith(
|
|
216
|
+
'/nocobase/v2/apps/test-app/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fadmin%2Fal5yj9t81of',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should whitelist sub-app signin only and reject main / other-app / v1 signin urls', () => {
|
|
221
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
222
|
+
configurable: true,
|
|
223
|
+
value: {
|
|
224
|
+
...originalLocation,
|
|
225
|
+
origin: 'http://localhost:20000',
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const app = {
|
|
230
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
231
|
+
router: {
|
|
232
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
233
|
+
},
|
|
234
|
+
} as any;
|
|
235
|
+
|
|
236
|
+
// own sub-app signin: accepted
|
|
237
|
+
expect(
|
|
238
|
+
resolveV2SigninRedirect(
|
|
239
|
+
'/nocobase/v2/apps/test-app/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fadmin',
|
|
240
|
+
app,
|
|
241
|
+
),
|
|
242
|
+
).toBe(
|
|
243
|
+
'http://localhost:20000/nocobase/v2/apps/test-app/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fadmin',
|
|
244
|
+
);
|
|
245
|
+
// bare /signin: still accepted (universal fallback)
|
|
246
|
+
expect(resolveV2SigninRedirect('/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fadmin', app)).toBe(
|
|
247
|
+
'http://localhost:20000/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app%2Fadmin',
|
|
248
|
+
);
|
|
249
|
+
// main v2 signin: rejected (would lose sub-app context)
|
|
250
|
+
expect(resolveV2SigninRedirect('/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin', app)).toBeNull();
|
|
251
|
+
// another sub-app signin: rejected (cross-app injection)
|
|
252
|
+
expect(
|
|
253
|
+
resolveV2SigninRedirect('/nocobase/v2/apps/other-app/signin?redirect=%2Fnocobase%2Fv2%2Fapps%2Ftest-app', app),
|
|
254
|
+
).toBeNull();
|
|
255
|
+
// v1 root signin: rejected
|
|
256
|
+
expect(resolveV2SigninRedirect('/nocobase/signin?redirect=%2Fnocobase%2Fv2', app)).toBeNull();
|
|
257
|
+
// cross-origin: rejected
|
|
258
|
+
expect(resolveV2SigninRedirect('https://evil.example.com/nocobase/v2/apps/test-app/signin', app)).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('fallback when router or basename is missing', () => {
|
|
263
|
+
it('should fall back to v2 public path when app.router is undefined', () => {
|
|
264
|
+
const app = {
|
|
265
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
266
|
+
router: undefined,
|
|
267
|
+
} as any;
|
|
268
|
+
|
|
269
|
+
expect(buildV2SigninHref(app, '/nocobase/v2/admin/X')).toBe(
|
|
270
|
+
'/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin%2FX',
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should fall back to v2 public path when router.getBasename returns undefined', () => {
|
|
275
|
+
const app = {
|
|
276
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
277
|
+
router: {
|
|
278
|
+
getBasename: () => undefined,
|
|
279
|
+
},
|
|
280
|
+
} as any;
|
|
281
|
+
|
|
282
|
+
expect(buildV2SigninHref(app, '/nocobase/v2/admin/X')).toBe(
|
|
283
|
+
'/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin%2FX',
|
|
284
|
+
);
|
|
285
|
+
});
|
|
180
286
|
});
|
|
181
287
|
});
|
|
@@ -26,6 +26,8 @@ describe('client-v2 defineGlobalDeps', () => {
|
|
|
26
26
|
expect(define).toHaveBeenCalledWith('@nocobase/client-v2', expect.any(Function));
|
|
27
27
|
expect(define).toHaveBeenCalledWith('@nocobase/flow-engine', expect.any(Function));
|
|
28
28
|
expect(define).toHaveBeenCalledWith('@nocobase/evaluators/client', expect.any(Function));
|
|
29
|
+
expect(define).toHaveBeenCalledWith('@dnd-kit/core', expect.any(Function));
|
|
30
|
+
expect(define).toHaveBeenCalledWith('@dnd-kit/sortable', expect.any(Function));
|
|
29
31
|
expect(define).toHaveBeenCalledWith('ahooks', expect.any(Function));
|
|
30
32
|
expect(define).toHaveBeenCalledWith('dayjs', expect.any(Function));
|
|
31
33
|
expect(define).toHaveBeenCalledWith('lodash', expect.any(Function));
|
|
@@ -39,7 +39,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
39
39
|
vi.restoreAllMocks();
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('should redirect unauthenticated admin access to
|
|
42
|
+
it('should redirect unauthenticated admin access to v2 signin with replace', async () => {
|
|
43
43
|
const replace = vi.fn();
|
|
44
44
|
Object.defineProperty(globalThis.window, 'location', {
|
|
45
45
|
configurable: true,
|
|
@@ -70,11 +70,11 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
70
70
|
render(<Root />);
|
|
71
71
|
|
|
72
72
|
await waitFor(() => {
|
|
73
|
-
expect(replace).toHaveBeenCalledWith('/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
73
|
+
expect(replace).toHaveBeenCalledWith('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
it('should redirect unauthenticated v2 root access to
|
|
77
|
+
it('should redirect unauthenticated v2 root access to v2 signin with default admin redirect', async () => {
|
|
78
78
|
const replace = vi.fn();
|
|
79
79
|
Object.defineProperty(globalThis.window, 'location', {
|
|
80
80
|
configurable: true,
|
|
@@ -104,11 +104,11 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
104
104
|
render(<Root />);
|
|
105
105
|
|
|
106
106
|
await waitFor(() => {
|
|
107
|
-
expect(replace).toHaveBeenCalledWith('/nocobase/signin?redirect=%2Fnocobase%2Fv2%2Fadmin');
|
|
107
|
+
expect(replace).toHaveBeenCalledWith('/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin');
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it('should render v2 admin root without redirecting
|
|
111
|
+
it('should render v2 admin root without redirecting away', async () => {
|
|
112
112
|
const replace = vi.fn();
|
|
113
113
|
Object.defineProperty(globalThis.window, 'location', {
|
|
114
114
|
configurable: true,
|
|
@@ -157,7 +157,7 @@ describe('nocobase buildin plugin auth redirect', () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
it.each(['/v2/admin/legacy-page/tab/tab-1', '/v2/admin/legacy-page/view/detail'])(
|
|
160
|
-
'should show 404 for authenticated direct
|
|
160
|
+
'should show 404 for authenticated direct v1-style v2 page access: %s',
|
|
161
161
|
async (pathname) => {
|
|
162
162
|
const replace = vi.fn();
|
|
163
163
|
Object.defineProperty(globalThis.window, 'location', {
|
package/src/authRedirect.ts
CHANGED
|
@@ -21,8 +21,6 @@ type LocationLike = {
|
|
|
21
21
|
hash?: string;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
const V2_PUBLIC_PATH_SUFFIX = '/v2/';
|
|
25
|
-
|
|
26
24
|
function ensureLeadingSlash(value = '') {
|
|
27
25
|
if (!value) {
|
|
28
26
|
return '/';
|
|
@@ -111,18 +109,18 @@ function getV2PublicPath(app: AppLike) {
|
|
|
111
109
|
return normalizePublicPath(app.getPublicPath());
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
function getRootPublicPath(app: AppLike) {
|
|
115
|
-
const publicPath = getV2PublicPath(app);
|
|
116
|
-
if (!publicPath.endsWith(V2_PUBLIC_PATH_SUFFIX)) {
|
|
117
|
-
return normalizePublicPath(publicPath.replace(/\/v2\/?$/, '/') || '/');
|
|
118
|
-
}
|
|
119
|
-
return normalizePublicPath(publicPath.slice(0, -V2_PUBLIC_PATH_SUFFIX.length) || '/');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
112
|
function getV2BasePath(app: AppLike) {
|
|
123
113
|
return trimTrailingSlashes(getV2PublicPath(app)) || '/';
|
|
124
114
|
}
|
|
125
115
|
|
|
116
|
+
export function getV2EffectiveBasePath(app: AppLike): string {
|
|
117
|
+
const basename = app.router?.getBasename?.();
|
|
118
|
+
if (basename) {
|
|
119
|
+
return normalizePublicPath(basename);
|
|
120
|
+
}
|
|
121
|
+
return getV2PublicPath(app);
|
|
122
|
+
}
|
|
123
|
+
|
|
126
124
|
function joinRootRelativePath(basePath: string, pathname: string) {
|
|
127
125
|
const normalizedBasePath = normalizePublicPath(basePath);
|
|
128
126
|
const normalizedPathname = normalizePathname(pathname);
|
|
@@ -138,46 +136,8 @@ function joinRootRelativePath(basePath: string, pathname: string) {
|
|
|
138
136
|
return normalizePathname(`/${trimLeadingSlashes(normalizedBasePath)}/${trimLeadingSlashes(normalizedPathname)}`);
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (normalizedPathname === '/admin') {
|
|
145
|
-
return '/admin';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const segments = normalizedPathname.split('/').filter(Boolean);
|
|
149
|
-
if (segments[0] !== 'admin' || segments.length < 2) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const [, name, ...restSegments] = segments;
|
|
154
|
-
const legacySegments = ['admin', name];
|
|
155
|
-
let index = 0;
|
|
156
|
-
|
|
157
|
-
while (index < restSegments.length) {
|
|
158
|
-
const segment = restSegments[index];
|
|
159
|
-
|
|
160
|
-
if (segment === 'tab' && restSegments[index + 1]) {
|
|
161
|
-
legacySegments.push('tabs', restSegments[index + 1]);
|
|
162
|
-
index += 2;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (segment === 'view') {
|
|
167
|
-
legacySegments.push('popups', ...restSegments.slice(index + 1));
|
|
168
|
-
index = restSegments.length;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return normalizePathname(`/${legacySegments.join('/')}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function getLegacySigninPath(app: AppLike) {
|
|
179
|
-
const rootPublicPath = trimTrailingSlashes(getRootPublicPath(app));
|
|
180
|
-
return `${rootPublicPath}/signin`;
|
|
139
|
+
function getV2SigninPath(app: AppLike) {
|
|
140
|
+
return joinRootRelativePath(getV2EffectiveBasePath(app), '/signin');
|
|
181
141
|
}
|
|
182
142
|
|
|
183
143
|
function stripCurrentV2Basename(app: AppLike, pathname: string) {
|
|
@@ -198,28 +158,7 @@ function stripCurrentV2Basename(app: AppLike, pathname: string) {
|
|
|
198
158
|
}
|
|
199
159
|
|
|
200
160
|
function getDefaultV2AdminRedirectPath(app: AppLike) {
|
|
201
|
-
return joinRootRelativePath(
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* 将 v2 admin 路径转换为对应的 v1 admin 根相对路径。
|
|
206
|
-
*
|
|
207
|
-
* @param app 当前 v2 应用实例
|
|
208
|
-
* @param value 当前 v2 admin 路径或 location 对象
|
|
209
|
-
* @returns 对应的 v1 admin 根相对路径;若无法安全转换则返回 null
|
|
210
|
-
*/
|
|
211
|
-
export function convertV2AdminPathToLegacy(app: AppLike, value: string | LocationLike) {
|
|
212
|
-
const locationLike = splitPathLike(value);
|
|
213
|
-
const pathname = stripCurrentV2Basename(app, locationLike.pathname || '/');
|
|
214
|
-
const legacyPathname = mapV2AdminPathnameToLegacyPathname(pathname);
|
|
215
|
-
|
|
216
|
-
if (!legacyPathname) {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return `${joinRootRelativePath(getRootPublicPath(app), legacyPathname)}${normalizeSearch(
|
|
221
|
-
locationLike.search,
|
|
222
|
-
)}${normalizeHash(locationLike.hash)}`;
|
|
161
|
+
return joinRootRelativePath(getV2EffectiveBasePath(app), '/admin');
|
|
223
162
|
}
|
|
224
163
|
|
|
225
164
|
/**
|
|
@@ -233,29 +172,29 @@ export function getCurrentV2RedirectPath(app: AppLike, locationLike: LocationLik
|
|
|
233
172
|
const pathname = stripCurrentV2Basename(app, locationLike?.pathname || '/');
|
|
234
173
|
const search = normalizeSearch(locationLike?.search || '');
|
|
235
174
|
const hash = normalizeHash(locationLike?.hash || '');
|
|
236
|
-
return `${joinRootRelativePath(
|
|
175
|
+
return `${joinRootRelativePath(getV2EffectiveBasePath(app), pathname)}${search}${hash}`;
|
|
237
176
|
}
|
|
238
177
|
|
|
239
178
|
/**
|
|
240
|
-
* 构造跳转到
|
|
179
|
+
* 构造跳转到 v2 登录页的完整 href。
|
|
241
180
|
*
|
|
242
181
|
* @param app 当前 v2 应用实例
|
|
243
182
|
* @param targetPath 登录后回跳地址
|
|
244
|
-
* @returns 指向
|
|
183
|
+
* @returns 指向 v2 登录页的 href
|
|
245
184
|
*/
|
|
246
|
-
export function
|
|
247
|
-
return `${
|
|
185
|
+
export function buildV2SigninHref(app: AppLike, targetPath: string) {
|
|
186
|
+
return `${getV2SigninPath(app)}?redirect=${encodeURIComponent(targetPath)}`;
|
|
248
187
|
}
|
|
249
188
|
|
|
250
189
|
/**
|
|
251
|
-
* 通过整页跳转进入
|
|
190
|
+
* 通过整页跳转进入 v2 登录页。
|
|
252
191
|
*
|
|
253
192
|
* @param app 当前 v2 应用实例
|
|
254
193
|
* @param targetPath 登录后回跳地址
|
|
255
194
|
* @param options 跳转选项
|
|
256
195
|
*/
|
|
257
|
-
export function
|
|
258
|
-
const href =
|
|
196
|
+
export function redirectToV2Signin(app: AppLike, targetPath: string, options?: { replace?: boolean }) {
|
|
197
|
+
const href = buildV2SigninHref(app, targetPath);
|
|
259
198
|
if (options?.replace === false) {
|
|
260
199
|
window.location.href = href;
|
|
261
200
|
return;
|
|
@@ -264,13 +203,13 @@ export function redirectToLegacySignin(app: AppLike, targetPath: string, options
|
|
|
264
203
|
}
|
|
265
204
|
|
|
266
205
|
/**
|
|
267
|
-
* 解析并校验服务端返回的
|
|
206
|
+
* 解析并校验服务端返回的 v2 登录页地址。
|
|
268
207
|
*
|
|
269
208
|
* @param value 服务端返回的 redirect
|
|
270
209
|
* @param app 当前 v2 应用实例
|
|
271
|
-
* @returns 合法时返回完整同源 URL
|
|
210
|
+
* @returns 合法时返回完整同源 URL,否则返回 null
|
|
272
211
|
*/
|
|
273
|
-
export function
|
|
212
|
+
export function resolveV2SigninRedirect(value: string | undefined | null, app: AppLike) {
|
|
274
213
|
if (!value) {
|
|
275
214
|
return null;
|
|
276
215
|
}
|
|
@@ -286,7 +225,7 @@ export function resolveLegacySigninRedirect(value: string | undefined | null, ap
|
|
|
286
225
|
return null;
|
|
287
226
|
}
|
|
288
227
|
|
|
289
|
-
const validPathnames = new Set(['/signin',
|
|
228
|
+
const validPathnames = new Set(['/signin', getV2SigninPath(app)]);
|
|
290
229
|
if (!validPathnames.has(url.pathname)) {
|
|
291
230
|
return null;
|
|
292
231
|
}
|