@nocobase/client-v2 2.1.0-beta.33 → 2.1.0-beta.35

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.
Files changed (76) hide show
  1. package/es/APIClient.d.ts +16 -0
  2. package/es/Application.d.ts +2 -1
  3. package/es/BaseApplication.d.ts +6 -0
  4. package/es/PluginManager.d.ts +2 -0
  5. package/es/authRedirect.d.ts +9 -16
  6. package/es/components/form/EnvVariableInput.d.ts +8 -6
  7. package/es/components/form/VariableInput.d.ts +73 -0
  8. package/es/components/form/index.d.ts +1 -0
  9. package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
  10. package/es/components/form/table/SelectionCell.d.ts +36 -0
  11. package/es/components/form/table/Table.d.ts +82 -0
  12. package/es/components/form/table/constants.d.ts +15 -0
  13. package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
  14. package/es/components/form/table/dnd/index.d.ts +9 -0
  15. package/es/components/form/table/index.d.ts +9 -0
  16. package/es/components/form/table/styles.d.ts +41 -0
  17. package/es/components/form/table/utils.d.ts +44 -0
  18. package/es/components/index.d.ts +2 -0
  19. package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  21. package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
  22. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
  23. package/es/flow-compat/passwordUtils.d.ts +1 -1
  24. package/es/index.d.ts +1 -0
  25. package/es/index.mjs +166 -99
  26. package/es/json-logic/globalOperators.d.ts +11 -0
  27. package/es/theme/globalStyles.d.ts +9 -0
  28. package/es/theme/index.d.ts +1 -0
  29. package/es/utils/globalDeps.d.ts +7 -0
  30. package/lib/index.js +173 -106
  31. package/package.json +9 -6
  32. package/src/APIClient.ts +68 -0
  33. package/src/Application.tsx +6 -2
  34. package/src/BaseApplication.tsx +8 -0
  35. package/src/PluginManager.ts +2 -0
  36. package/src/__tests__/app.test.tsx +8 -0
  37. package/src/__tests__/authRedirect.test.ts +170 -64
  38. package/src/__tests__/globalDeps.test.ts +2 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
  40. package/src/__tests__/remotePlugins.test.ts +148 -0
  41. package/src/authRedirect.ts +23 -84
  42. package/src/components/form/EnvVariableInput.tsx +11 -46
  43. package/src/components/form/VariableInput.tsx +177 -0
  44. package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
  45. package/src/components/form/index.tsx +1 -0
  46. package/src/components/form/table/RowOverlayPreview.tsx +51 -0
  47. package/src/components/form/table/SelectionCell.tsx +72 -0
  48. package/src/components/form/table/Table.tsx +279 -0
  49. package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
  50. package/src/components/form/table/constants.ts +16 -0
  51. package/src/components/form/table/dnd/SortableRow.tsx +106 -0
  52. package/src/components/form/table/dnd/index.ts +10 -0
  53. package/src/components/form/table/index.tsx +13 -0
  54. package/src/components/form/table/styles.ts +110 -0
  55. package/src/components/form/table/utils.ts +75 -0
  56. package/src/components/index.ts +2 -0
  57. package/src/css-variable/CSSVariableProvider.tsx +1 -1
  58. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  59. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
  61. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
  62. package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
  63. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
  64. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
  65. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  66. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  67. package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
  68. package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
  69. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
  70. package/src/index.ts +1 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +4 -4
  73. package/src/theme/globalStyles.ts +21 -0
  74. package/src/theme/index.tsx +1 -0
  75. package/src/utils/globalDeps.ts +50 -30
  76. package/src/utils/remotePlugins.ts +107 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.0-beta.33",
3
+ "version": "2.1.0-beta.35",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -20,14 +20,17 @@
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.33",
28
- "@nocobase/flow-engine": "2.1.0-beta.33",
29
- "@nocobase/sdk": "2.1.0-beta.33",
30
- "@nocobase/shared": "2.1.0-beta.33",
29
+ "@nocobase/evaluators": "2.1.0-beta.35",
30
+ "@nocobase/flow-engine": "2.1.0-beta.35",
31
+ "@nocobase/sdk": "2.1.0-beta.35",
32
+ "@nocobase/shared": "2.1.0-beta.35",
33
+ "@nocobase/utils": "2.1.0-beta.35",
31
34
  "ahooks": "^3.7.2",
32
35
  "antd": "5.24.2",
33
36
  "antd-style": "3.7.1",
@@ -41,5 +44,5 @@
41
44
  "react-i18next": "^11.15.1",
42
45
  "react-router-dom": "^6.30.1"
43
46
  },
44
- "gitHead": "4815c394e80a264fa8ed619246280923c47aeb72"
47
+ "gitHead": "74310d8b9e9581fcde14b5a93d12b41ddb5bb325"
45
48
  }
@@ -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
+ }
@@ -7,10 +7,11 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { APIClient, type APIClientOptions } from '@nocobase/sdk';
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(options.apiClient);
47
+ return new APIClient({
48
+ ...options.apiClient,
49
+ appName: options.name || getSubAppName(options.publicPath),
50
+ });
47
51
  }
48
52
 
49
53
  protected configureRuntimeAdapters() {
@@ -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() {
@@ -26,6 +26,8 @@ export type PluginData = {
26
26
  version: string;
27
27
  url: string;
28
28
  clientV2Url?: string;
29
+ devMode?: 'esm';
30
+ appDevDependencies?: string[];
29
31
  type: 'local' | 'upload' | 'npm';
30
32
  };
31
33
 
@@ -48,6 +48,14 @@ describe('app', () => {
48
48
  expect(app.getHref('/test')).toBe('/test');
49
49
  });
50
50
 
51
+ it('should initialize shared jsonLogic operators', () => {
52
+ const app = new Application({ router });
53
+
54
+ expect(app.jsonLogic.apply({ $eq: [1, '1'] })).toBe(true);
55
+ app.jsonLogic.addOperation('$testAlwaysTrue', () => true);
56
+ expect(app.jsonLogic.apply({ $testAlwaysTrue: [] })).toBe(true);
57
+ });
58
+
51
59
  it('should apply the provided favicon immediately', () => {
52
60
  const app = new Application({ router });
53
61
 
@@ -8,11 +8,10 @@
8
8
  */
9
9
 
10
10
  import {
11
- buildLegacySigninHref,
12
- convertV2AdminPathToLegacy,
11
+ buildV2SigninHref,
13
12
  getCurrentV2RedirectPath,
14
- redirectToLegacySignin,
15
- resolveLegacySigninRedirect,
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 legacy signin href from root public path', () => {
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(buildLegacySigninHref(app, '/nocobase/v2/admin/7vu4c2sdk6h')).toBe(
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 convert v2 admin path to legacy root-relative path', () => {
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(convertV2AdminPathToLegacy(app, '/v2/admin/page-1')).toBe('/admin/page-1');
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
- redirectToLegacySignin(app, '/v2/admin/7vu4c2sdk6h');
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 legacy signin urls only', () => {
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(resolveLegacySigninRedirect('/nocobase/signin?redirect=%2Fnocobase%2Fv2%2Fadmin#hash', app)).toBe(
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(resolveLegacySigninRedirect('/signin?redirect=%2Fv2%2Fadmin', app)).toBe(
127
+ expect(resolveV2SigninRedirect('/signin?redirect=%2Fv2%2Fadmin', app)).toBe(
175
128
  'http://localhost:20000/signin?redirect=%2Fv2%2Fadmin',
176
129
  );
177
- expect(resolveLegacySigninRedirect('/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin', app)).toBeNull();
178
- expect(resolveLegacySigninRedirect('https://evil.example.com/signin?redirect=%2Fv2%2Fadmin', app)).toBeNull();
179
- expect(resolveLegacySigninRedirect('/nocobase/admin', app)).toBeNull();
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 legacy signin with replace', async () => {
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 legacy signin with default admin redirect', async () => {
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 to legacy default page', async () => {
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 legacy v2 page access: %s',
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', {