@nocobase/client-v2 2.3.0-beta.6 → 2.4.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 (90) hide show
  1. package/es/acl/ACLProvider.d.ts +10 -0
  2. package/es/acl/aclCheckReadiness.d.ts +11 -0
  3. package/es/authRedirect.d.ts +11 -2
  4. package/es/components/AttachmentUpload.d.ts +51 -0
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  7. package/es/index.d.ts +4 -1
  8. package/es/index.mjs +129 -118
  9. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  10. package/es/nocobase-buildin-plugin/index.d.ts +10 -0
  11. package/es/settings-app/SettingsApplication.d.ts +18 -0
  12. package/es/settings-app/SettingsBrand.d.ts +16 -0
  13. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  14. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  15. package/es/settings-app/SettingsGroupNav.d.ts +18 -0
  16. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  17. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  18. package/es/settings-app/SettingsSearch.d.ts +17 -0
  19. package/es/settings-app/SettingsShell.d.ts +10 -0
  20. package/es/settings-app/isSettingsApp.d.ts +18 -0
  21. package/es/settings-app/runtimePaths.d.ts +18 -0
  22. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  23. package/es/settings-app/settingsTheme.d.ts +87 -0
  24. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  25. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  26. package/es/settings-center/groups.d.ts +66 -0
  27. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  28. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  29. package/es/settings-center/utils.d.ts +24 -0
  30. package/es/theme/index.d.ts +1 -0
  31. package/es/theme/itemActive.d.ts +24 -0
  32. package/es/theme/type.d.ts +9 -0
  33. package/lib/index.js +157 -146
  34. package/package.json +7 -7
  35. package/src/__tests__/app.test.tsx +122 -3
  36. package/src/__tests__/authRedirect.test.ts +154 -0
  37. package/src/__tests__/browserChecker.test.ts +172 -6
  38. package/src/__tests__/mockSettingsApplication.ts +29 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +81 -8
  40. package/src/__tests__/plugin-manager.test.tsx +7 -6
  41. package/src/__tests__/settings-application.test.ts +164 -0
  42. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  43. package/src/__tests__/settings-center.test.tsx +168 -130
  44. package/src/__tests__/settings-layout-root.test.tsx +600 -0
  45. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  46. package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
  47. package/src/__tests__/settings-shell.test.tsx +261 -0
  48. package/src/acl/ACLProvider.tsx +53 -3
  49. package/src/acl/aclCheckReadiness.ts +44 -0
  50. package/src/authRedirect.ts +85 -8
  51. package/src/components/AppComponents.tsx +16 -7
  52. package/src/components/AttachmentUpload.tsx +275 -0
  53. package/src/components/index.ts +1 -0
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -56
  55. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  56. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  57. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  58. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  59. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  60. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  61. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  62. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  63. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  64. package/src/index.ts +11 -1
  65. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  66. package/src/nocobase-buildin-plugin/index.tsx +112 -71
  67. package/src/settings-app/SettingsApplication.ts +31 -0
  68. package/src/settings-app/SettingsBrand.tsx +108 -0
  69. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  70. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  71. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  72. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  73. package/src/settings-app/SettingsRouterManager.ts +92 -0
  74. package/src/settings-app/SettingsSearch.tsx +343 -0
  75. package/src/settings-app/SettingsShell.tsx +184 -0
  76. package/src/settings-app/isSettingsApp.ts +21 -0
  77. package/src/settings-app/runtimePaths.ts +103 -0
  78. package/src/settings-app/settingsDocumentPath.ts +66 -0
  79. package/src/settings-app/settingsTheme.ts +369 -0
  80. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  81. package/src/settings-center/AdminSettingsLayout.tsx +154 -169
  82. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  83. package/src/settings-center/__tests__/groups.test.ts +25 -0
  84. package/src/settings-center/groups.ts +108 -0
  85. package/src/settings-center/useSettingsGroups.ts +105 -0
  86. package/src/settings-center/useSettingsSearch.ts +173 -0
  87. package/src/settings-center/utils.tsx +50 -1
  88. package/src/theme/index.tsx +1 -0
  89. package/src/theme/itemActive.ts +31 -0
  90. package/src/theme/type.ts +10 -0
@@ -7,21 +7,17 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import {
11
- ACLRolesCheckProvider,
12
- createMockClient,
13
- NocoBaseDesktopRouteType,
14
- type NocoBaseDesktopRoute,
15
- Plugin,
16
- } from '@nocobase/client-v2';
17
- import { act, render, screen, waitFor, fireEvent } from '@testing-library/react';
10
+ import { ACLRolesCheckProvider, Plugin } from '@nocobase/client-v2';
11
+ import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
18
12
  import React from 'react';
19
13
  import { message } from 'antd';
20
14
  import { vi } from 'vitest';
21
15
  import { AdminSettingsLayoutModel as ClientV2AdminSettingsLayoutModel } from '../settings-center';
22
16
  import { AdminSettingsLayoutModel as ClientV1AdminSettingsLayoutModel } from '../../../client/src/pm/AdminSettingsLayoutModel';
23
- import { NocoBaseBuildInPlugin } from '../nocobase-buildin-plugin';
17
+ import zhCN from '../../../client/src/locale/zh-CN.json';
18
+ import { SettingsBuildInPlugin } from '../settings-app/SettingsBuildInPlugin';
24
19
  import { matchSettingsRoute, sortTopLevelSettings } from '../settings-center/utils';
20
+ import { createMockSettingsClient } from './mockSettingsApplication';
25
21
 
26
22
  class TestAclPlugin extends Plugin {
27
23
  async load() {
@@ -29,7 +25,7 @@ class TestAclPlugin extends Plugin {
29
25
  }
30
26
  }
31
27
 
32
- type MockClientApplication = ReturnType<typeof createMockClient>;
28
+ type MockClientApplication = ReturnType<typeof createMockSettingsClient>;
33
29
 
34
30
  const renderApp = async (app: MockClientApplication) => {
35
31
  const Root = app.getRootComponent();
@@ -50,17 +46,19 @@ const waitForGetRequests = async (app: MockClientApplication, urls: string[]) =>
50
46
  const mockAdminRuntime = (
51
47
  app: MockClientApplication,
52
48
  options: {
49
+ lang?: string;
53
50
  snippets?: string[];
54
51
  pmList?: any[];
52
+ resources?: Record<string, string>;
55
53
  systemSettings?: Record<string, any>;
56
- desktopRoutes?: NocoBaseDesktopRoute[];
57
54
  } = {},
58
55
  ) => {
59
56
  const {
57
+ lang = 'en-US',
60
58
  snippets = ['pm', 'pm.system-settings.system-settings'],
61
59
  pmList = [],
60
+ resources = {},
62
61
  systemSettings = {},
63
- desktopRoutes = [],
64
62
  } = options;
65
63
 
66
64
  app.dataSourceManager.getCollection = ((name: string, collectionName: string) => {
@@ -86,9 +84,9 @@ const mockAdminRuntime = (
86
84
  });
87
85
  app.apiMock.onGet('app:getLang').reply(200, {
88
86
  data: {
89
- lang: 'en-US',
87
+ lang,
90
88
  resources: {
91
- client: {},
89
+ client: resources,
92
90
  },
93
91
  cron: {},
94
92
  },
@@ -105,7 +103,7 @@ const mockAdminRuntime = (
105
103
  },
106
104
  });
107
105
  app.apiMock.onGet('/desktopRoutes:listAccessible').reply(200, {
108
- data: desktopRoutes,
106
+ data: [],
109
107
  });
110
108
  app.apiMock.onGet('systemSettings:get').reply(200, {
111
109
  data: {
@@ -187,10 +185,10 @@ describe('settings center', () => {
187
185
  expect(sortTopLevelSettings(settings).map((item) => item.name)).toEqual(['api-keys', 'backups', 'system-settings']);
188
186
  });
189
187
 
190
- it('should redirect /admin/settings to system-settings by default', async () => {
191
- const app = createMockClient({
192
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
193
- router: { type: 'memory', initialEntries: ['/admin/settings'] },
188
+ it('should redirect /settings to system-settings by default', async () => {
189
+ const app = createMockSettingsClient({
190
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
191
+ router: { type: 'memory', initialEntries: ['/settings'] },
194
192
  });
195
193
  mockAdminRuntime(app);
196
194
 
@@ -201,9 +199,9 @@ describe('settings center', () => {
201
199
  });
202
200
 
203
201
  it('should expose current language variable as enabled-language selector', async () => {
204
- const app = createMockClient({
205
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
206
- router: { type: 'memory', initialEntries: ['/admin/settings/system-settings'] },
202
+ const app = createMockSettingsClient({
203
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
204
+ router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
207
205
  });
208
206
  mockAdminRuntime(app, {
209
207
  systemSettings: {
@@ -231,9 +229,9 @@ describe('settings center', () => {
231
229
  });
232
230
 
233
231
  it('should fallback to plugin-manager when system-settings is not allowed', async () => {
234
- const app = createMockClient({
235
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
236
- router: { type: 'memory', initialEntries: ['/admin/settings'] },
232
+ const app = createMockSettingsClient({
233
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
234
+ router: { type: 'memory', initialEntries: ['/settings'] },
237
235
  });
238
236
  mockAdminRuntime(app, {
239
237
  snippets: ['pm', '!pm.system-settings.system-settings'],
@@ -256,43 +254,54 @@ describe('settings center', () => {
256
254
  expect(await screen.findByText('Demo plugin')).toBeInTheDocument();
257
255
  });
258
256
 
259
- it('should redirect to the first accessible page when the role cannot access settings', async () => {
260
- const app = createMockClient({
261
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
262
- router: { type: 'memory', initialEntries: ['/admin/settings/system-settings'] },
263
- });
264
- mockAdminRuntime(app, {
265
- snippets: ['!pm', '!pm.system-settings.system-settings'],
266
- desktopRoutes: [
267
- {
268
- id: 1,
269
- schemaUid: 'first-accessible-page',
270
- title: 'First accessible page',
271
- type: NocoBaseDesktopRouteType.flowPage,
272
- },
273
- {
274
- id: 2,
275
- schemaUid: 'second-accessible-page',
276
- title: 'Second accessible page',
277
- type: NocoBaseDesktopRouteType.flowPage,
278
- },
279
- ],
257
+ it('should redirect to the admin app when the role cannot access settings', async () => {
258
+ const originalLocation = globalThis.window.location;
259
+ const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
260
+ const replace = vi.fn();
261
+ Object.defineProperty(globalThis.window, 'location', {
262
+ configurable: true,
263
+ value: {
264
+ ...originalLocation,
265
+ replace,
266
+ },
280
267
  });
268
+ window.__nocobase_modern_client_prefix__ = 'v';
281
269
 
282
- await renderApp(app);
283
- await waitForGetRequests(app, ['/auth:check', 'roles:check', '/desktopRoutes:listAccessible']);
270
+ try {
271
+ const app = createMockSettingsClient({
272
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
273
+ router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
274
+ });
275
+ mockAdminRuntime(app, {
276
+ snippets: ['!pm', '!pm.system-settings.system-settings'],
277
+ });
284
278
 
285
- await waitFor(() => {
286
- expect(app.router.state.location.pathname).toBe('/admin/first-accessible-page');
287
- });
288
- expect(app.router.state.historyAction).toBe('REPLACE');
289
- expect(screen.queryByText('Current settings page is unavailable')).not.toBeInTheDocument();
279
+ await renderApp(app);
280
+ await waitForGetRequests(app, ['/auth:check', 'roles:check']);
281
+
282
+ await waitFor(() => {
283
+ expect(replace).toHaveBeenCalledWith('/v/admin');
284
+ });
285
+ expect(
286
+ screen.queryByRole('heading', { name: 'Your current role cannot access Settings' }),
287
+ ).not.toBeInTheDocument();
288
+ } finally {
289
+ Object.defineProperty(globalThis.window, 'location', {
290
+ configurable: true,
291
+ value: originalLocation,
292
+ });
293
+ if (originalModernClientPrefix === undefined) {
294
+ delete window.__nocobase_modern_client_prefix__;
295
+ } else {
296
+ window.__nocobase_modern_client_prefix__ = originalModernClientPrefix;
297
+ }
298
+ }
290
299
  });
291
300
 
292
301
  it('should hide plugin-manager menu item when pm snippet is missing', async () => {
293
- const app = createMockClient({
294
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
295
- router: { type: 'memory', initialEntries: ['/admin/settings/system-settings'] },
302
+ const app = createMockSettingsClient({
303
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
304
+ router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
296
305
  });
297
306
  mockAdminRuntime(app, {
298
307
  snippets: ['pm.system-settings.system-settings'],
@@ -306,27 +315,23 @@ describe('settings center', () => {
306
315
  });
307
316
 
308
317
  it('should show route empty state for unknown settings routes', async () => {
309
- const app = createMockClient({
310
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
311
- router: { type: 'memory', initialEntries: ['/admin/settings/unknown'] },
318
+ const app = createMockSettingsClient({
319
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
320
+ router: { type: 'memory', initialEntries: ['/settings/unknown'] },
312
321
  });
313
322
  mockAdminRuntime(app, {
314
323
  snippets: ['!pm', '!pm.system-settings.system-settings'],
315
- desktopRoutes: [
316
- {
317
- id: 1,
318
- schemaUid: 'first-accessible-page',
319
- title: 'First accessible page',
320
- type: NocoBaseDesktopRouteType.flowPage,
321
- },
322
- ],
323
324
  });
324
325
 
325
326
  await renderApp(app);
326
327
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
327
328
 
328
- expect(await screen.findByText('Current settings page is unavailable')).toBeInTheDocument();
329
- expect(app.router.state.location.pathname).toBe('/admin/settings/unknown');
329
+ const result = await screen.findByRole('status');
330
+ expect(within(result).getByRole('heading', { name: 'Settings page not found' })).toBeInTheDocument();
331
+ expect(
332
+ within(result).getByText('The settings page you requested does not exist or has been removed.'),
333
+ ).toBeInTheDocument();
334
+ expect(app.router.state.location.pathname).toBe('/settings/unknown');
330
335
  });
331
336
 
332
337
  it('should allow direct access to hidden page without showing menu entry', async () => {
@@ -343,27 +348,18 @@ describe('settings center', () => {
343
348
  }
344
349
  }
345
350
 
346
- const app = createMockClient({
347
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, HiddenSettingsPlugin],
348
- router: { type: 'memory', initialEntries: ['/admin/settings/hidden-demo'] },
349
- });
350
- mockAdminRuntime(app, {
351
- desktopRoutes: [
352
- {
353
- id: 1,
354
- schemaUid: 'first-accessible-page',
355
- title: 'First accessible page',
356
- type: NocoBaseDesktopRouteType.flowPage,
357
- },
358
- ],
351
+ const app = createMockSettingsClient({
352
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, HiddenSettingsPlugin],
353
+ router: { type: 'memory', initialEntries: ['/settings/hidden-demo'] },
359
354
  });
355
+ mockAdminRuntime(app);
360
356
 
361
357
  await renderApp(app);
362
358
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
363
359
 
364
360
  expect(await screen.findByText('Hidden settings page')).toBeInTheDocument();
365
361
  expect(screen.queryByRole('menuitem', { name: 'Hidden demo' })).not.toBeInTheDocument();
366
- expect(app.router.state.location.pathname).toBe('/admin/settings/hidden-demo');
362
+ expect(app.router.state.location.pathname).toBe('/settings/hidden-demo');
367
363
  });
368
364
 
369
365
  it('should show route empty state when direct access page has no permission', async () => {
@@ -380,28 +376,24 @@ describe('settings center', () => {
380
376
  }
381
377
  }
382
378
 
383
- const app = createMockClient({
384
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ProtectedSettingsPlugin],
385
- router: { type: 'memory', initialEntries: ['/admin/settings/secure-demo'] },
379
+ const app = createMockSettingsClient({
380
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, ProtectedSettingsPlugin],
381
+ router: { type: 'memory', initialEntries: ['/settings/secure-demo'] },
386
382
  });
387
383
  mockAdminRuntime(app, {
384
+ lang: 'zh-CN',
385
+ resources: zhCN,
388
386
  snippets: ['pm', 'pm.system-settings.system-settings', '!pm.secure-demo.index'],
389
- desktopRoutes: [
390
- {
391
- id: 1,
392
- schemaUid: 'first-accessible-page',
393
- title: 'First accessible page',
394
- type: NocoBaseDesktopRouteType.flowPage,
395
- },
396
- ],
397
387
  });
398
388
 
399
389
  await renderApp(app);
400
390
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
401
391
 
402
- expect(await screen.findByText('Current settings page is unavailable')).toBeInTheDocument();
392
+ const result = await screen.findByRole('status');
393
+ expect(within(result).getByRole('heading', { name: '当前角色无权访问设置中心' })).toBeInTheDocument();
394
+ expect(within(result).getByText('请切换至有权限的角色,或联系管理员获取访问权限。')).toBeInTheDocument();
403
395
  expect(screen.queryByText('Secure settings page')).not.toBeInTheDocument();
404
- expect(app.router.state.location.pathname).toBe('/admin/settings/secure-demo');
396
+ expect(app.router.state.location.pathname).toBe('/settings/secure-demo');
405
397
  });
406
398
 
407
399
  it('should redirect a denied settings tab to the first accessible tab', async () => {
@@ -437,9 +429,9 @@ describe('settings center', () => {
437
429
  }
438
430
  }
439
431
 
440
- const app = createMockClient({
441
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ProtectedSettingsTabsPlugin],
442
- router: { type: 'memory', initialEntries: ['/admin/settings/protected-tabs/denied'] },
432
+ const app = createMockSettingsClient({
433
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, ProtectedSettingsTabsPlugin],
434
+ router: { type: 'memory', initialEntries: ['/settings/protected-tabs/denied'] },
443
435
  });
444
436
  mockAdminRuntime(app, {
445
437
  snippets: ['pm', '!pm.protected-tabs.denied'],
@@ -449,7 +441,7 @@ describe('settings center', () => {
449
441
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
450
442
 
451
443
  await waitFor(() => {
452
- expect(app.router.router.state.location.pathname).toBe('/admin/settings/protected-tabs/z-first');
444
+ expect(app.router.router.state.location.pathname).toBe('/settings/protected-tabs/z-first');
453
445
  });
454
446
  expect(app.router.router.state.historyAction).toBe('REPLACE');
455
447
  expect(await screen.findByText('First accessible tab content')).toBeInTheDocument();
@@ -457,13 +449,54 @@ describe('settings center', () => {
457
449
  expect(renderDeniedTab).not.toHaveBeenCalled();
458
450
 
459
451
  await act(async () => {
460
- await app.router.router.navigate('/admin/settings/protected-tabs/a-second');
452
+ await app.router.router.navigate('/settings/protected-tabs/a-second');
461
453
  });
462
454
 
463
455
  expect(await screen.findByText('Second accessible tab content')).toBeInTheDocument();
464
- expect(app.router.router.state.location.pathname).toBe('/admin/settings/protected-tabs/a-second');
456
+ expect(app.router.router.state.location.pathname).toBe('/settings/protected-tabs/a-second');
465
457
  });
466
458
 
459
+ it.each(['/settings/fallback-access', '/settings/fallback-access/'])(
460
+ 'should redirect a denied index page to an accessible sibling from %s',
461
+ async (initialEntry) => {
462
+ class FallbackAccessSettingsPlugin extends Plugin {
463
+ async load() {
464
+ this.pluginSettingsManager.addMenuItem({ key: 'fallback-access', title: 'Fallback access' });
465
+ this.pluginSettingsManager.addPageTabItem({
466
+ menuKey: 'fallback-access',
467
+ key: 'index',
468
+ title: 'Restricted index settings',
469
+ aclSnippet: 'pm.fallback-access.index',
470
+ Component: () => <div>Restricted index settings page</div>,
471
+ });
472
+ this.pluginSettingsManager.addPageTabItem({
473
+ menuKey: 'fallback-access',
474
+ key: 'allowed',
475
+ title: 'Allowed fallback settings',
476
+ Component: () => <div>Allowed fallback settings page</div>,
477
+ });
478
+ }
479
+ }
480
+
481
+ const app = createMockSettingsClient({
482
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, FallbackAccessSettingsPlugin],
483
+ router: { type: 'memory', initialEntries: [initialEntry] },
484
+ });
485
+ mockAdminRuntime(app, {
486
+ snippets: ['pm', 'pm.system-settings.system-settings', '!pm.fallback-access.index'],
487
+ });
488
+
489
+ await renderApp(app);
490
+ await waitForGetRequests(app, ['/auth:check', 'roles:check']);
491
+
492
+ expect(await screen.findByText('Allowed fallback settings page')).toBeInTheDocument();
493
+ expect(
494
+ screen.queryByRole('heading', { name: 'Your current role cannot access Settings' }),
495
+ ).not.toBeInTheDocument();
496
+ expect(screen.queryByText('Restricted index settings page')).not.toBeInTheDocument();
497
+ },
498
+ );
499
+
467
500
  it('should skip an accessible dynamic tab when its route params cannot be resolved', async () => {
468
501
  class DynamicSettingsTabsPlugin extends Plugin {
469
502
  async load() {
@@ -495,9 +528,9 @@ describe('settings center', () => {
495
528
  }
496
529
  }
497
530
 
498
- const app = createMockClient({
499
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, DynamicSettingsTabsPlugin],
500
- router: { type: 'memory', initialEntries: ['/admin/settings/dynamic-tabs/denied'] },
531
+ const app = createMockSettingsClient({
532
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, DynamicSettingsTabsPlugin],
533
+ router: { type: 'memory', initialEntries: ['/settings/dynamic-tabs/denied'] },
501
534
  });
502
535
  mockAdminRuntime(app, {
503
536
  snippets: ['pm', '!pm.dynamic-tabs.denied'],
@@ -507,7 +540,7 @@ describe('settings center', () => {
507
540
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
508
541
 
509
542
  await waitFor(() => {
510
- expect(app.router.router.state.location.pathname).toBe('/admin/settings/dynamic-tabs/fallback');
543
+ expect(app.router.router.state.location.pathname).toBe('/settings/dynamic-tabs/fallback');
511
544
  });
512
545
  expect(await screen.findByText('Static fallback tab content')).toBeInTheDocument();
513
546
  });
@@ -535,9 +568,9 @@ describe('settings center', () => {
535
568
  }
536
569
  }
537
570
 
538
- const app = createMockClient({
539
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, DynamicSiblingTabsPlugin],
540
- router: { type: 'memory', initialEntries: ['/admin/settings/dynamic-siblings/email:primary/logs'] },
571
+ const app = createMockSettingsClient({
572
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, DynamicSiblingTabsPlugin],
573
+ router: { type: 'memory', initialEntries: ['/settings/dynamic-siblings/email:primary/logs'] },
541
574
  });
542
575
  mockAdminRuntime(app, {
543
576
  snippets: ['pm', '!pm.dynamic-siblings.logs'],
@@ -547,7 +580,7 @@ describe('settings center', () => {
547
580
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
548
581
 
549
582
  await waitFor(() => {
550
- expect(app.router.router.state.location.pathname).toBe('/admin/settings/dynamic-siblings/email:primary/channels');
583
+ expect(app.router.router.state.location.pathname).toBe('/settings/dynamic-siblings/email:primary/channels');
551
584
  });
552
585
  expect(await screen.findByText('Dynamic channels tab content')).toBeInTheDocument();
553
586
  });
@@ -575,9 +608,9 @@ describe('settings center', () => {
575
608
  }
576
609
  }
577
610
 
578
- const app = createMockClient({
579
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, HyphenatedParamTabsPlugin],
580
- router: { type: 'memory', initialEntries: ['/admin/settings/hyphenated-params/email/logs'] },
611
+ const app = createMockSettingsClient({
612
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, HyphenatedParamTabsPlugin],
613
+ router: { type: 'memory', initialEntries: ['/settings/hyphenated-params/email/logs'] },
581
614
  });
582
615
  mockAdminRuntime(app, {
583
616
  snippets: ['pm', '!pm.hyphenated-params.logs'],
@@ -587,7 +620,7 @@ describe('settings center', () => {
587
620
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
588
621
 
589
622
  await waitFor(() => {
590
- expect(app.router.router.state.location.pathname).toBe('/admin/settings/hyphenated-params/email/channels');
623
+ expect(app.router.router.state.location.pathname).toBe('/settings/hyphenated-params/email/channels');
591
624
  });
592
625
  expect(await screen.findByText('Hyphenated channels tab content')).toBeInTheDocument();
593
626
  });
@@ -609,9 +642,9 @@ describe('settings center', () => {
609
642
  }
610
643
  }
611
644
 
612
- const app = createMockClient({
613
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, MenuAclPlugin],
614
- router: { type: 'memory', initialEntries: ['/admin/settings/menu-acl-demo'] },
645
+ const app = createMockSettingsClient({
646
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, MenuAclPlugin],
647
+ router: { type: 'memory', initialEntries: ['/settings/menu-acl-demo'] },
615
648
  });
616
649
  mockAdminRuntime(app, {
617
650
  snippets: ['pm', 'pm.system-settings.system-settings', '!pm.menu-acl-demo.menu'],
@@ -621,10 +654,13 @@ describe('settings center', () => {
621
654
  await waitForGetRequests(app, ['/auth:check', 'roles:check']);
622
655
 
623
656
  expect(await screen.findByText('Menu ACL child page')).toBeInTheDocument();
624
- expect(screen.getByRole('menuitem', { name: 'Menu ACL Demo' })).toBeInTheDocument();
657
+ // The sidebar is gone; this group is reached from the "other settings" hover
658
+ // dropdown in the top bar. The dropdown only renders on hover, so this just
659
+ // checks the group entry survived (ACL did not drop the whole menu).
660
+ expect(screen.getByRole('menuitem', { name: 'Other settings' })).toBeInTheDocument();
625
661
  });
626
662
 
627
- it('should allow the settings sidebar menu to scroll independently', async () => {
663
+ it('should keep rendering the active page when a group has many settings', async () => {
628
664
  class ManySettingsPlugin extends Plugin {
629
665
  async load() {
630
666
  for (let index = 0; index < 30; index += 1) {
@@ -642,9 +678,9 @@ describe('settings center', () => {
642
678
  }
643
679
  }
644
680
 
645
- const app = createMockClient({
646
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin, ManySettingsPlugin],
647
- router: { type: 'memory', initialEntries: ['/admin/settings/scroll-demo-29'] },
681
+ const app = createMockSettingsClient({
682
+ plugins: [SettingsBuildInPlugin, TestAclPlugin, ManySettingsPlugin],
683
+ router: { type: 'memory', initialEntries: ['/settings/scroll-demo-29'] },
648
684
  });
649
685
  mockAdminRuntime(app);
650
686
 
@@ -653,14 +689,16 @@ describe('settings center', () => {
653
689
 
654
690
  expect(await screen.findByText('Scroll demo page 29')).toBeInTheDocument();
655
691
 
656
- const sidebar = screen.getByRole('menuitem', { name: 'Scroll demo 29' }).closest('.ant-layout-sider');
657
- expect(sidebar).toHaveStyle({ overflowY: 'auto' });
692
+ // Without the sidebar there is no separate scroll area: all 30 settings live in
693
+ // the top bar dropdown and the page renders only the active one, so this just
694
+ // checks it still holds up.
695
+ expect(document.querySelector('.ant-layout-sider')).toBeNull();
658
696
  });
659
697
 
660
698
  it('should save system settings through systemSettings:put', async () => {
661
- const app = createMockClient({
662
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
663
- router: { type: 'memory', initialEntries: ['/admin/settings/system-settings'] },
699
+ const app = createMockSettingsClient({
700
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
701
+ router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
664
702
  });
665
703
  mockAdminRuntime(app);
666
704
 
@@ -677,9 +715,9 @@ describe('settings center', () => {
677
715
  });
678
716
 
679
717
  it('should block invalid logo uploads by storage rules', async () => {
680
- const app = createMockClient({
681
- plugins: [NocoBaseBuildInPlugin, TestAclPlugin],
682
- router: { type: 'memory', initialEntries: ['/admin/settings/system-settings'] },
718
+ const app = createMockSettingsClient({
719
+ plugins: [SettingsBuildInPlugin, TestAclPlugin],
720
+ router: { type: 'memory', initialEntries: ['/settings/system-settings'] },
683
721
  });
684
722
  const messageErrorSpy = vi.spyOn(message, 'error').mockImplementation(() => {
685
723
  return undefined as any;