@nocobase/client-v2 2.3.0-alpha.1 → 3.0.0-alpha.2

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 (123) hide show
  1. package/es/acl/ACLProvider.d.ts +10 -0
  2. package/es/authRedirect.d.ts +5 -0
  3. package/es/components/AttachmentUpload.d.ts +51 -0
  4. package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/actions/linkageRules.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  8. package/es/flow/components/FlowRoute.d.ts +2 -2
  9. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  10. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  11. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  12. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  13. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  14. package/es/index.d.ts +4 -1
  15. package/es/index.mjs +139 -125
  16. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  17. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  18. package/es/settings-app/SettingsApplication.d.ts +18 -0
  19. package/es/settings-app/SettingsBrand.d.ts +16 -0
  20. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  21. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  22. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  23. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  24. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  25. package/es/settings-app/SettingsSearch.d.ts +17 -0
  26. package/es/settings-app/SettingsShell.d.ts +10 -0
  27. package/es/settings-app/isSettingsApp.d.ts +18 -0
  28. package/es/settings-app/runtimePaths.d.ts +18 -0
  29. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  30. package/es/settings-app/settingsTheme.d.ts +87 -0
  31. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  32. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  33. package/es/settings-center/groups.d.ts +66 -0
  34. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  35. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  36. package/es/settings-center/utils.d.ts +24 -0
  37. package/es/theme/index.d.ts +1 -0
  38. package/es/theme/itemActive.d.ts +24 -0
  39. package/es/theme/type.d.ts +9 -0
  40. package/lib/index.js +141 -127
  41. package/lib/locale/languageCodes.js +1 -0
  42. package/package.json +8 -7
  43. package/src/__tests__/authRedirect.test.ts +137 -0
  44. package/src/__tests__/browserChecker.test.ts +124 -6
  45. package/src/__tests__/mockSettingsApplication.ts +29 -0
  46. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +51 -10
  47. package/src/__tests__/plugin-manager.test.tsx +7 -6
  48. package/src/__tests__/settings-application.test.ts +164 -0
  49. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  50. package/src/__tests__/settings-center.test.tsx +38 -37
  51. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  52. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  53. package/src/__tests__/settings-shell.test.tsx +200 -0
  54. package/src/acl/ACLProvider.tsx +21 -0
  55. package/src/authRedirect.ts +72 -3
  56. package/src/components/AttachmentUpload.tsx +275 -0
  57. package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
  58. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  59. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
  60. package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
  61. package/src/components/index.ts +1 -0
  62. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  63. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  64. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  65. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  66. package/src/flow/actions/linkageRules.tsx +26 -6
  67. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
  68. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  69. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FlowRoute.tsx +71 -29
  74. package/src/flow/index.ts +2 -0
  75. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  76. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  77. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  78. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  79. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  80. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  81. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  82. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  83. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  84. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  85. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  86. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  87. package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
  88. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  89. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  90. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  91. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  92. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  93. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  94. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  95. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  96. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  97. package/src/index.ts +10 -1
  98. package/src/locale/languageCodes.ts +1 -0
  99. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  100. package/src/nocobase-buildin-plugin/index.tsx +99 -71
  101. package/src/settings-app/SettingsApplication.ts +31 -0
  102. package/src/settings-app/SettingsBrand.tsx +108 -0
  103. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  104. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  105. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  106. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  107. package/src/settings-app/SettingsRouterManager.ts +92 -0
  108. package/src/settings-app/SettingsSearch.tsx +238 -0
  109. package/src/settings-app/SettingsShell.tsx +167 -0
  110. package/src/settings-app/isSettingsApp.ts +21 -0
  111. package/src/settings-app/runtimePaths.ts +104 -0
  112. package/src/settings-app/settingsDocumentPath.ts +66 -0
  113. package/src/settings-app/settingsTheme.ts +301 -0
  114. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  115. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  116. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  117. package/src/settings-center/groups.ts +108 -0
  118. package/src/settings-center/useSettingsGroups.ts +105 -0
  119. package/src/settings-center/useSettingsSearch.ts +173 -0
  120. package/src/settings-center/utils.tsx +50 -1
  121. package/src/theme/index.tsx +1 -0
  122. package/src/theme/itemActive.ts +31 -0
  123. package/src/theme/type.ts +10 -0
@@ -64,6 +64,7 @@ const languageCodes = {
64
64
  "ml-IN": { label: "\u0D2E\u0D32\u0D2F\u0D3E\u0D33\u0D02" },
65
65
  "mn-MN": { label: "\u041C\u043E\u043D\u0433\u043E\u043B \u0445\u044D\u043B" },
66
66
  "ms-MY": { label: "\u0628\u0647\u0627\u0633 \u0645\u0644\u0627\u064A\u0648" },
67
+ "my-MM": { label: "\u1019\u103C\u1014\u103A\u1019\u102C\u1018\u102C\u101E\u102C" },
67
68
  "nb-NO": { label: "Norsk bokm\xE5l" },
68
69
  "ne-NP": { label: "\u0928\u0947\u092A\u093E\u0932\u0940" },
69
70
  "nl-BE": { label: "Vlaams" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.3.0-alpha.1",
3
+ "version": "3.0.0-alpha.2",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.3.0-alpha.1",
31
- "@nocobase/flow-engine": "2.3.0-alpha.1",
32
- "@nocobase/sdk": "2.3.0-alpha.1",
33
- "@nocobase/shared": "2.3.0-alpha.1",
34
- "@nocobase/utils": "2.3.0-alpha.1",
30
+ "@nocobase/evaluators": "3.0.0-alpha.2",
31
+ "@nocobase/flow-engine": "3.0.0-alpha.2",
32
+ "@nocobase/sdk": "3.0.0-alpha.2",
33
+ "@nocobase/shared": "3.0.0-alpha.2",
34
+ "@nocobase/utils": "3.0.0-alpha.2",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -44,8 +44,9 @@
44
44
  "json5": "^2.2.3",
45
45
  "jsqr": "^1.4.0",
46
46
  "lodash": "4.17.21",
47
+ "react-device-detect": "2.2.3",
47
48
  "react-i18next": "^11.15.1",
48
49
  "react-router-dom": "^6.30.1"
49
50
  },
50
- "gitHead": "2377df8ceb12549149017f7f14a61207bf6e49a2"
51
+ "gitHead": "a3faea21082b30989301de04591ee1b91fd4e6a5"
51
52
  }
@@ -17,12 +17,18 @@ import {
17
17
 
18
18
  describe('auth redirect helpers', () => {
19
19
  const originalLocation = globalThis.window.location;
20
+ const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
20
21
 
21
22
  afterEach(() => {
22
23
  Object.defineProperty(globalThis.window, 'location', {
23
24
  configurable: true,
24
25
  value: originalLocation,
25
26
  });
27
+ if (originalModernClientPrefix === undefined) {
28
+ delete window.__nocobase_modern_client_prefix__;
29
+ } else {
30
+ window.__nocobase_modern_client_prefix__ = originalModernClientPrefix;
31
+ }
26
32
  vi.restoreAllMocks();
27
33
  });
28
34
 
@@ -84,6 +90,107 @@ describe('auth redirect helpers', () => {
84
90
  expect(buildV2SigninHref(app, '/v2/admin/7vu4c2sdk6h')).toBe('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
85
91
  });
86
92
 
93
+ it('should keep validated standalone settings redirects outside the v2 basename', () => {
94
+ const app = {
95
+ getPublicPath: () => '/nocobase/v2/',
96
+ router: {
97
+ getBasename: () => '/nocobase/v2',
98
+ },
99
+ } as any;
100
+
101
+ expect(normalizeV2RedirectPath(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
102
+ '/nocobase/settings/workflow?tab=list#recent',
103
+ );
104
+ });
105
+
106
+ it('should build the standalone settings signin URL for a standalone settings app', () => {
107
+ const app = {
108
+ name: 'main',
109
+ getPublicPath: () => '/nocobase/',
110
+ router: {
111
+ getBasename: () => '/nocobase',
112
+ },
113
+ pluginSettingsManager: {
114
+ getRouteName: () => 'settings.',
115
+ getRoutePath: () => '/settings/',
116
+ },
117
+ } satisfies Parameters<typeof buildV2SigninHref>[0];
118
+ expect(buildV2SigninHref(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
119
+ '/nocobase/settings/signin?redirect=%2Fnocobase%2Fsettings%2Fworkflow%3Ftab%3Dlist%23recent',
120
+ );
121
+ });
122
+
123
+ it('should keep an apps segment inside the main public path out of the Settings application scope', () => {
124
+ const app = {
125
+ name: 'main',
126
+ getPublicPath: () => '/tenant/apps/root/',
127
+ router: {
128
+ getBasename: () => '/tenant/apps/root/',
129
+ },
130
+ pluginSettingsManager: {
131
+ getRouteName: () => 'settings.',
132
+ getRoutePath: () => '/settings/',
133
+ },
134
+ } satisfies Parameters<typeof buildV2SigninHref>[0];
135
+
136
+ expect(buildV2SigninHref(app, '/tenant/apps/root/settings/workflow')).toBe(
137
+ '/tenant/apps/root/settings/signin?redirect=%2Ftenant%2Fapps%2Froot%2Fsettings%2Fworkflow',
138
+ );
139
+ });
140
+
141
+ it.each([
142
+ ['/nocobase/settings/apps/test-app/', '/nocobase/settings/apps/test-app/signin'],
143
+ ['/nocobase/settings/_app/test-app/', '/nocobase/settings/_app/test-app/signin'],
144
+ ])('should keep the standalone settings signin in the current sub-app scope: %s', (basename, signinPath) => {
145
+ const app = {
146
+ name: 'test-app',
147
+ getPublicPath: () => '/nocobase/',
148
+ router: {
149
+ getBasename: () => basename,
150
+ },
151
+ pluginSettingsManager: {
152
+ getRouteName: () => 'settings.',
153
+ getRoutePath: () => '/',
154
+ },
155
+ } as any;
156
+
157
+ expect(buildV2SigninHref(app, `${basename}workflow`)).toBe(
158
+ `${signinPath}?redirect=${encodeURIComponent(`${basename}workflow`)}`,
159
+ );
160
+ });
161
+
162
+ it('should accept the current standalone settings signin URL only', () => {
163
+ Object.defineProperty(globalThis.window, 'location', {
164
+ configurable: true,
165
+ value: {
166
+ ...originalLocation,
167
+ origin: 'http://localhost:20000',
168
+ },
169
+ });
170
+ const app = {
171
+ name: 'test-app',
172
+ getPublicPath: () => '/nocobase/',
173
+ router: {
174
+ getBasename: () => '/nocobase/settings/apps/test-app/',
175
+ },
176
+ pluginSettingsManager: {
177
+ getRouteName: () => 'settings.',
178
+ getRoutePath: () => '/',
179
+ },
180
+ } as any;
181
+
182
+ expect(
183
+ resolveV2SigninRedirect(
184
+ '/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
185
+ app,
186
+ ),
187
+ ).toBe(
188
+ 'http://localhost:20000/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
189
+ );
190
+ expect(resolveV2SigninRedirect('/nocobase/v/apps/test-app/signin', app)).toBeNull();
191
+ expect(resolveV2SigninRedirect('/nocobase/settings/apps/other-app/signin', app)).toBeNull();
192
+ });
193
+
87
194
  it('should redirect with window.location.replace by default', () => {
88
195
  const replace = vi.fn();
89
196
  Object.defineProperty(globalThis.window, 'location', {
@@ -134,6 +241,36 @@ describe('auth redirect helpers', () => {
134
241
  });
135
242
 
136
243
  describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
244
+ it('should keep only the current sub-app standalone settings redirect', () => {
245
+ const app = {
246
+ getPublicPath: () => '/nocobase/v2/',
247
+ router: {
248
+ getBasename: () => '/nocobase/v2/apps/test-app/',
249
+ },
250
+ } as any;
251
+
252
+ expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/test-app/workflow')).toBe(
253
+ '/nocobase/settings/apps/test-app/workflow',
254
+ );
255
+ expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/other-app/workflow')).toBe(
256
+ '/nocobase/v2/apps/test-app/admin/',
257
+ );
258
+ });
259
+
260
+ it.each([
261
+ '/nocobase/settings/apps/test-app/../../other-app/settings',
262
+ '/nocobase/settings/apps/test-app/%2e%2e/%2E%2e/other-app/settings',
263
+ ])('should reject a standalone settings redirect that resolves outside the current sub-app: %s', (target) => {
264
+ const app = {
265
+ getPublicPath: () => '/nocobase/v2/',
266
+ router: {
267
+ getBasename: () => '/nocobase/v2/apps/test-app/',
268
+ },
269
+ } as any;
270
+
271
+ expect(normalizeV2RedirectPath(app, target)).toBe('/nocobase/v2/apps/test-app/admin/');
272
+ });
273
+
137
274
  it('should normalize signin redirect fallback under the current sub-app basename', () => {
138
275
  const app = {
139
276
  getPublicPath: () => '/v/',
@@ -15,8 +15,11 @@ import { fileURLToPath } from 'node:url';
15
15
  type BrowserCheckerCase = {
16
16
  pathname: string;
17
17
  publicPath: string;
18
+ search?: string;
19
+ hash?: string;
18
20
  modernClientPrefix?: string;
19
21
  appClientEntryMode?: string;
22
+ scriptSrc?: string;
20
23
  expectedRedirect?: string;
21
24
  };
22
25
 
@@ -47,8 +50,8 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
47
50
  location: {
48
51
  origin: 'http://c.local.nocobase.com',
49
52
  pathname: input.pathname,
50
- search: '',
51
- hash: '',
53
+ search: input.search || '',
54
+ hash: input.hash || '',
52
55
  replace,
53
56
  },
54
57
  console: consoleMock,
@@ -58,6 +61,13 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
58
61
  onresize: undefined,
59
62
  },
60
63
  document: {
64
+ currentScript: {
65
+ src:
66
+ input.scriptSrc ||
67
+ (scriptPath.includes('/app/client-v2/public/')
68
+ ? 'http://assets.local.nocobase.com/v/browser-checker.js?v=1'
69
+ : 'http://assets.local.nocobase.com/browser-checker.js?v=1'),
70
+ },
61
71
  documentElement: {
62
72
  className: '',
63
73
  clientWidth: 1280,
@@ -80,11 +90,11 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
80
90
  describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
81
91
  it('normalizes a relative public path before redirecting to the trailing-slash entry', () => {
82
92
  const replace = executeBrowserChecker(scriptPath, {
83
- pathname: '/nocobase/v',
84
- publicPath: 'nocobase/v/',
93
+ pathname: '/nocobase/console',
94
+ publicPath: 'nocobase/console/',
85
95
  });
86
96
 
87
- expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
97
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/console/');
88
98
  });
89
99
 
90
100
  it('prefixes outside paths with a root-relative basename instead of duplicating a relative segment', () => {
@@ -98,7 +108,7 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
98
108
 
99
109
  it('does not redirect when the current path is already under the normalized basename', () => {
100
110
  const replace = executeBrowserChecker(scriptPath, {
101
- pathname: '/nocobase/v/',
111
+ pathname: '/nocobase/v/admin',
102
112
  publicPath: 'nocobase/v/',
103
113
  });
104
114
 
@@ -170,4 +180,112 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
170
180
  expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/apps/a_31itq60q4kg/admin');
171
181
  });
172
182
  }
183
+
184
+ if (scriptPath.includes('/app/client-v2/public/')) {
185
+ it.each(['/v', '/v/'])('redirects the main modern root %s to Settings', (pathname) => {
186
+ const replace = executeBrowserChecker(scriptPath, {
187
+ pathname,
188
+ publicPath: '/v/',
189
+ modernClientPrefix: 'v',
190
+ });
191
+
192
+ expect(replace).toHaveBeenCalledOnce();
193
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/settings');
194
+ });
195
+
196
+ it.each([
197
+ ['/v/apps/demo', '/settings/apps/demo'],
198
+ ['/v/apps/demo/', '/settings/apps/demo'],
199
+ ['/v/_app/demo', '/settings/_app/demo'],
200
+ ['/v/_app/demo/', '/settings/_app/demo'],
201
+ ])('redirects the scoped modern root %s to %s', (pathname, expectedPathname) => {
202
+ const replace = executeBrowserChecker(scriptPath, {
203
+ pathname,
204
+ publicPath: '/v/',
205
+ modernClientPrefix: 'v',
206
+ });
207
+
208
+ expect(replace).toHaveBeenCalledOnce();
209
+ expect(replace).toHaveBeenCalledWith(`http://c.local.nocobase.com${expectedPathname}`);
210
+ });
211
+
212
+ it.each([
213
+ ['/settings/apps/demo', '/', '/settings/apps/demo/', 'v'],
214
+ ['/settings/_app/demo', '/', '/settings/_app/demo/', 'v'],
215
+ ['/nocobase/settings/apps/demo', '/nocobase/', '/nocobase/settings/apps/demo/', 'v'],
216
+ ['/nocobase/settings/_app/demo', '/nocobase/', '/nocobase/settings/_app/demo/', 'v'],
217
+ ['/tenant/apps/root/settings/apps/demo', '/tenant/apps/root/', '/tenant/apps/root/settings/apps/demo/', 'v'],
218
+ ['/tenant/_app/root/settings/_app/demo', '/tenant/_app/root/', '/tenant/_app/root/settings/_app/demo/', 'v'],
219
+ ['/tenant/v/settings/apps/demo', '/tenant/v/', '/tenant/v/settings/apps/demo/', 'v'],
220
+ ['/tenant/modern/settings/_app/demo', '/tenant/modern/', '/tenant/modern/settings/_app/demo/', 'modern'],
221
+ ])('normalizes the scoped Settings root %s to %s', (pathname, publicPath, expectedPathname, modernClientPrefix) => {
222
+ const replace = executeBrowserChecker(scriptPath, {
223
+ pathname,
224
+ publicPath,
225
+ modernClientPrefix,
226
+ scriptSrc: 'https://cdn.example.com/ui/settings/browser-checker.js?v=1',
227
+ search: '?tab=overview',
228
+ hash: '#panel',
229
+ });
230
+
231
+ expect(replace).toHaveBeenCalledOnce();
232
+ expect(replace).toHaveBeenCalledWith(`http://c.local.nocobase.com${expectedPathname}?tab=overview#panel`);
233
+ });
234
+
235
+ it.each([
236
+ ['/tenant/apps/root/modern/apps/demo/', '/tenant/apps/root/modern/', '/tenant/apps/root/settings/apps/demo'],
237
+ ['/tenant/_app/root/modern/_app/demo/', '/tenant/_app/root/modern/', '/tenant/_app/root/settings/_app/demo'],
238
+ ])(
239
+ 'preserves public path, query, and hash for %s without treating public-path segments as app scope',
240
+ (pathname, publicPath, expectedPathname) => {
241
+ const replace = executeBrowserChecker(scriptPath, {
242
+ pathname,
243
+ publicPath,
244
+ modernClientPrefix: 'modern',
245
+ search: '?tab=overview',
246
+ hash: '#panel',
247
+ });
248
+
249
+ expect(replace).toHaveBeenCalledOnce();
250
+ expect(replace).toHaveBeenCalledWith(`http://c.local.nocobase.com${expectedPathname}?tab=overview#panel`);
251
+ },
252
+ );
253
+
254
+ it.each([
255
+ ['/v/admin', '/v/'],
256
+ ['/v/apps/demo/admin', '/v/'],
257
+ ['/v/_app/demo/admin', '/v/'],
258
+ ['/v/_apps/demo', '/v/'],
259
+ ['/v/settings/apps/demo', '/v/'],
260
+ ['/nocobase/modern/settings/_app/demo', '/nocobase/modern/', 'modern'],
261
+ ])('does not redirect a non-root modern path %s to Settings', (pathname, publicPath, modernClientPrefix = 'v') => {
262
+ const replace = executeBrowserChecker(scriptPath, {
263
+ pathname,
264
+ publicPath,
265
+ modernClientPrefix,
266
+ });
267
+
268
+ expect(replace).not.toHaveBeenCalled();
269
+ });
270
+
271
+ it.each([
272
+ ['/settings', '/'],
273
+ ['/settings/', '/'],
274
+ ['/settings/apps/demo/', '/'],
275
+ ['/settings/_app/demo/', '/'],
276
+ ['/settings/apps/demo/multi-portal', '/'],
277
+ ['/settings/_app/demo/multi-portal', '/'],
278
+ ['/settings/_apps/demo', '/'],
279
+ ['/nocobase/settings/apps/demo/multi-portal', '/nocobase/'],
280
+ ])('does not redirect a non-target Settings path %s', (pathname, publicPath) => {
281
+ const replace = executeBrowserChecker(scriptPath, {
282
+ pathname,
283
+ publicPath,
284
+ modernClientPrefix: 'v',
285
+ scriptSrc: 'https://cdn.example.com/ui/settings/browser-checker.js?v=1',
286
+ });
287
+
288
+ expect(replace).not.toHaveBeenCalled();
289
+ });
290
+ }
173
291
  });
@@ -0,0 +1,29 @@
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 MockAdapter from 'axios-mock-adapter';
11
+ import type { ApplicationOptions } from '../Application';
12
+ import { SettingsApplication } from '../settings-app/SettingsApplication';
13
+
14
+ class MockSettingsApplication extends SettingsApplication {
15
+ readonly apiMock: MockAdapter;
16
+
17
+ constructor(options: ApplicationOptions = {}) {
18
+ super({
19
+ router: { type: 'memory', initialEntries: ['/settings'] },
20
+ ws: false,
21
+ ...options,
22
+ });
23
+ this.apiMock = new MockAdapter(this.apiClient.axios);
24
+ }
25
+ }
26
+
27
+ export function createMockSettingsClient(options?: ApplicationOptions) {
28
+ return new MockSettingsApplication(options);
29
+ }
@@ -51,6 +51,27 @@ class AuthBootstrapRoutePlugin extends Plugin {
51
51
  }
52
52
  }
53
53
 
54
+ class TestAdminPortalPlugin extends Plugin {
55
+ async load() {
56
+ this.app.layoutManager.registerLayout({
57
+ routeName: 'admin',
58
+ routePath: '/admin',
59
+ uid: 'admin-layout-model',
60
+ layoutModelClass: 'AdminLayoutModel',
61
+ });
62
+ }
63
+ }
64
+
65
+ class TestRootPortalPlugin extends Plugin {
66
+ async load() {
67
+ this.router.add('root', {
68
+ path: '/',
69
+ authCheck: true,
70
+ Component: () => <div>portal landing</div>,
71
+ });
72
+ }
73
+ }
74
+
54
75
  describe('nocobase buildin plugin auth redirect', () => {
55
76
  const originalLocation = globalThis.window.location;
56
77
 
@@ -85,6 +106,16 @@ describe('nocobase buildin plugin auth redirect', () => {
85
106
  vi.restoreAllMocks();
86
107
  });
87
108
 
109
+ it('should leave Admin portal registration to runtime plugins', async () => {
110
+ const app = createMockClient();
111
+ const plugin = new NocoBaseBuildInPlugin({}, app);
112
+
113
+ await plugin.load();
114
+
115
+ expect(app.layoutManager.hasLayout('admin')).toBe(false);
116
+ expect(app.router.has('root')).toBe(false);
117
+ });
118
+
88
119
  it('should navigate to v2 signin when /auth:check returns no user', async () => {
89
120
  // Aligns with v1: use react-router navigate (virtual) rather than
90
121
  // `window.location.replace`, so a `window.location.href` queued elsewhere
@@ -92,7 +123,7 @@ describe('nocobase buildin plugin auth redirect', () => {
92
123
  // overridden.
93
124
  const app = createMockClient({
94
125
  publicPath: '/v2/',
95
- plugins: [NocoBaseBuildInPlugin as any],
126
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
96
127
  router: { type: 'memory', initialEntries: ['/v2/admin/7vu4c2sdk6h'] },
97
128
  });
98
129
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -115,7 +146,7 @@ describe('nocobase buildin plugin auth redirect', () => {
115
146
  // and race the 2FA response interceptor with its own signin redirect.
116
147
  const app = createMockClient({
117
148
  publicPath: '/v2/',
118
- plugins: [NocoBaseBuildInPlugin as any],
149
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
119
150
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
120
151
  });
121
152
  app.apiClient.auth.setToken('test-token');
@@ -136,29 +167,34 @@ describe('nocobase buildin plugin auth redirect', () => {
136
167
  expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(0);
137
168
  });
138
169
 
139
- it('should redirect unauthenticated v2 root access to v2 signin via <Navigate />', async () => {
170
+ it('should redirect an unauthenticated Portal root to v2 signin', async () => {
140
171
  const app = createMockClient({
141
172
  publicPath: '/nocobase/v2/',
142
- plugins: [NocoBaseBuildInPlugin as any],
173
+ plugins: [NocoBaseBuildInPlugin as any, TestRootPortalPlugin as typeof Plugin],
143
174
  router: { type: 'memory', initialEntries: ['/nocobase/v2/'] },
144
175
  });
145
176
  app.apiMock.onGet('app:getLang').reply(200, {
146
177
  data: { lang: 'en-US', resources: { client: {} }, cron: {} },
147
178
  });
179
+ app.apiMock.onGet('/auth:check').reply(200, { data: {} });
148
180
 
149
181
  const Root = app.getRootComponent();
150
182
  render(<Root />);
151
183
 
152
184
  await waitFor(() => {
153
185
  expect(app.router.router.state.location.pathname).toBe('/nocobase/v2/signin');
154
- expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2%2Fadmin');
186
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2');
155
187
  });
156
188
  });
157
189
 
158
190
  it('should check current user after navigating from skipped route to v2 admin', async () => {
159
191
  const app = createMockClient({
160
192
  publicPath: '/v2/',
161
- plugins: [NocoBaseBuildInPlugin as any, SkippedPublicRoutePlugin as any],
193
+ plugins: [
194
+ NocoBaseBuildInPlugin as any,
195
+ TestAdminPortalPlugin as typeof Plugin,
196
+ SkippedPublicRoutePlugin as typeof Plugin,
197
+ ],
162
198
  router: { type: 'memory', initialEntries: ['/v2/public'] },
163
199
  });
164
200
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -385,7 +421,7 @@ describe('nocobase buildin plugin auth redirect', () => {
385
421
  it('should render v2 admin root without redirecting away', async () => {
386
422
  const app = createMockClient({
387
423
  publicPath: '/v2/',
388
- plugins: [NocoBaseBuildInPlugin as any],
424
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
389
425
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
390
426
  });
391
427
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -415,11 +451,11 @@ describe('nocobase buildin plugin auth redirect', () => {
415
451
  });
416
452
 
417
453
  it.each(['/v2/admin/legacy-page/tab/tab-1', '/v2/admin/legacy-page/view/detail'])(
418
- 'should show 404 for authenticated direct v1-style v2 page access: %s',
454
+ 'should explain authenticated direct v1-style v2 page access: %s',
419
455
  async (pathname) => {
420
456
  const app = createMockClient({
421
457
  publicPath: '/v2/',
422
- plugins: [NocoBaseBuildInPlugin as any],
458
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
423
459
  router: { type: 'memory', initialEntries: [pathname] },
424
460
  });
425
461
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -441,7 +477,12 @@ describe('nocobase buildin plugin auth redirect', () => {
441
477
  const Root = app.getRootComponent();
442
478
  render(<Root />);
443
479
 
444
- expect(await screen.findByText('404')).toBeInTheDocument();
480
+ expect(await screen.findByText('This page is not supported in the /v2/ branch')).toBeInTheDocument();
481
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
482
+ expect(screen.getByRole('link', { name: 'Open from the original entry' })).toHaveAttribute(
483
+ 'href',
484
+ pathname.replace(/^\/v2/, ''),
485
+ );
445
486
  expect(app.router.router.state.location.pathname).toBe(pathname);
446
487
  },
447
488
  );
@@ -7,10 +7,11 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { ACLRolesCheckProvider, createMockClient, Plugin } from '@nocobase/client-v2';
10
+ import { ACLRolesCheckProvider, Plugin } from '@nocobase/client-v2';
11
11
  import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
12
12
  import React from 'react';
13
- import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
13
+ import { SettingsBuildInPlugin } from '../settings-app/SettingsBuildInPlugin';
14
+ import { createMockSettingsClient } from './mockSettingsApplication';
14
15
 
15
16
  class TestAclPlugin extends Plugin {
16
17
  async load() {
@@ -31,7 +32,7 @@ class TestSettingsLinkPlugin extends Plugin {
31
32
  }
32
33
  }
33
34
 
34
- type MockClientApplication = ReturnType<typeof createMockClient>;
35
+ type MockClientApplication = ReturnType<typeof createMockSettingsClient>;
35
36
 
36
37
  const renderApp = (app: MockClientApplication) => {
37
38
  const Root = app.getRootComponent();
@@ -49,9 +50,9 @@ const waitForGetRequests = async (app: MockClientApplication, urls: string[]) =>
49
50
  };
50
51
 
51
52
  const setupApp = (pmList: any[], plugins: Array<typeof Plugin> = []) => {
52
- const app = createMockClient({
53
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ...plugins],
54
- router: { type: 'memory', initialEntries: ['/admin/settings/plugin-manager'] },
53
+ const app = createMockSettingsClient({
54
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, ...plugins],
55
+ router: { type: 'memory', initialEntries: ['/settings/plugin-manager'] },
55
56
  });
56
57
 
57
58
  app.apiMock.onGet('/auth:check').reply(200, {