@nocobase/client-v2 2.3.0-beta.6 → 2.4.0-alpha.1

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
@@ -51,6 +51,42 @@ 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
+
75
+ class TestScopedPortalPlugin extends Plugin {
76
+ async load() {
77
+ this.router.add('customer-portal', {
78
+ path: '/customer/*',
79
+ authCheck: true,
80
+ Component: () => <div>customer portal</div>,
81
+ });
82
+ this.router.add('customer-portal-signin', {
83
+ path: '/customer/signin',
84
+ skipAuthCheck: true,
85
+ Component: () => <div>customer portal signin</div>,
86
+ });
87
+ }
88
+ }
89
+
54
90
  describe('nocobase buildin plugin auth redirect', () => {
55
91
  const originalLocation = globalThis.window.location;
56
92
 
@@ -85,6 +121,16 @@ describe('nocobase buildin plugin auth redirect', () => {
85
121
  vi.restoreAllMocks();
86
122
  });
87
123
 
124
+ it('should leave Admin portal registration to runtime plugins', async () => {
125
+ const app = createMockClient();
126
+ const plugin = new NocoBaseBuildInPlugin({}, app);
127
+
128
+ await plugin.load();
129
+
130
+ expect(app.layoutManager.hasLayout('admin')).toBe(false);
131
+ expect(app.router.has('root')).toBe(false);
132
+ });
133
+
88
134
  it('should navigate to v2 signin when /auth:check returns no user', async () => {
89
135
  // Aligns with v1: use react-router navigate (virtual) rather than
90
136
  // `window.location.replace`, so a `window.location.href` queued elsewhere
@@ -92,7 +138,7 @@ describe('nocobase buildin plugin auth redirect', () => {
92
138
  // overridden.
93
139
  const app = createMockClient({
94
140
  publicPath: '/v2/',
95
- plugins: [NocoBaseBuildInPlugin as any],
141
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
96
142
  router: { type: 'memory', initialEntries: ['/v2/admin/7vu4c2sdk6h'] },
97
143
  });
98
144
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -115,7 +161,7 @@ describe('nocobase buildin plugin auth redirect', () => {
115
161
  // and race the 2FA response interceptor with its own signin redirect.
116
162
  const app = createMockClient({
117
163
  publicPath: '/v2/',
118
- plugins: [NocoBaseBuildInPlugin as any],
164
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
119
165
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
120
166
  });
121
167
  app.apiClient.auth.setToken('test-token');
@@ -136,29 +182,56 @@ describe('nocobase buildin plugin auth redirect', () => {
136
182
  expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(0);
137
183
  });
138
184
 
139
- it('should redirect unauthenticated v2 root access to v2 signin via <Navigate />', async () => {
185
+ it('should redirect an unauthenticated Portal root to v2 signin', async () => {
140
186
  const app = createMockClient({
141
187
  publicPath: '/nocobase/v2/',
142
- plugins: [NocoBaseBuildInPlugin as any],
188
+ plugins: [NocoBaseBuildInPlugin as any, TestRootPortalPlugin as typeof Plugin],
143
189
  router: { type: 'memory', initialEntries: ['/nocobase/v2/'] },
144
190
  });
145
191
  app.apiMock.onGet('app:getLang').reply(200, {
146
192
  data: { lang: 'en-US', resources: { client: {} }, cron: {} },
147
193
  });
194
+ app.apiMock.onGet('/auth:check').reply(200, { data: {} });
148
195
 
149
196
  const Root = app.getRootComponent();
150
197
  render(<Root />);
151
198
 
152
199
  await waitFor(() => {
153
200
  expect(app.router.router.state.location.pathname).toBe('/nocobase/v2/signin');
154
- expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2%2Fadmin');
201
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2');
202
+ });
203
+ });
204
+
205
+ it('should use an explicitly registered Portal signin route when unauthenticated', async () => {
206
+ const app = createMockClient({
207
+ publicPath: '/v2/',
208
+ plugins: [NocoBaseBuildInPlugin as unknown as typeof Plugin, TestScopedPortalPlugin as typeof Plugin],
209
+ router: { type: 'memory', initialEntries: ['/v2/customer/dashboard?tab=overview#panel'] },
210
+ });
211
+ app.apiMock.onGet('app:getLang').reply(200, {
212
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
213
+ });
214
+ app.apiMock.onGet('/auth:check').reply(200, { data: {} });
215
+
216
+ const Root = app.getRootComponent();
217
+ render(<Root />);
218
+
219
+ await waitFor(() => {
220
+ expect(app.router.router.state.location.pathname).toBe('/v2/customer/signin');
221
+ expect(app.router.router.state.location.search).toBe(
222
+ '?redirect=%2Fv2%2Fcustomer%2Fdashboard%3Ftab%3Doverview%23panel',
223
+ );
155
224
  });
156
225
  });
157
226
 
158
227
  it('should check current user after navigating from skipped route to v2 admin', async () => {
159
228
  const app = createMockClient({
160
229
  publicPath: '/v2/',
161
- plugins: [NocoBaseBuildInPlugin as any, SkippedPublicRoutePlugin as any],
230
+ plugins: [
231
+ NocoBaseBuildInPlugin as any,
232
+ TestAdminPortalPlugin as typeof Plugin,
233
+ SkippedPublicRoutePlugin as typeof Plugin,
234
+ ],
162
235
  router: { type: 'memory', initialEntries: ['/v2/public'] },
163
236
  });
164
237
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -385,7 +458,7 @@ describe('nocobase buildin plugin auth redirect', () => {
385
458
  it('should render v2 admin root without redirecting away', async () => {
386
459
  const app = createMockClient({
387
460
  publicPath: '/v2/',
388
- plugins: [NocoBaseBuildInPlugin as any],
461
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
389
462
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
390
463
  });
391
464
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -419,7 +492,7 @@ describe('nocobase buildin plugin auth redirect', () => {
419
492
  async (pathname) => {
420
493
  const app = createMockClient({
421
494
  publicPath: '/v2/',
422
- plugins: [NocoBaseBuildInPlugin as any],
495
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
423
496
  router: { type: 'memory', initialEntries: [pathname] },
424
497
  });
425
498
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -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, {
@@ -0,0 +1,164 @@
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 { describe, expect, it } from 'vitest';
12
+ import { Application } from '../index';
13
+ import { SettingsApplication } from '../settings-app/SettingsApplication';
14
+ import { SettingsBuildInPlugin } from '../settings-app/SettingsBuildInPlugin';
15
+
16
+ describe('SettingsApplication', () => {
17
+ it('uses the standalone settings route namespace', () => {
18
+ const adminApp = new Application({ router: { type: 'memory' }, ws: false });
19
+ const settingsApp = new SettingsApplication({ router: { type: 'memory' }, ws: false });
20
+
21
+ expect(adminApp.pluginSettingsManager.getRouteName('demo.index')).toBe('admin.settings.demo.index');
22
+ expect(adminApp.pluginSettingsManager.getRoutePath('demo.index')).toBe('/admin/settings/demo');
23
+
24
+ expect(settingsApp.pluginSettingsManager.getRouteName('demo.index')).toBe('settings.demo.index');
25
+ expect(settingsApp.pluginSettingsManager.getRoutePath('demo.index')).toBe('/settings/demo');
26
+ expect(settingsApp.pluginSettingsManager.getRoutePath('')).toBe('/settings/');
27
+ });
28
+
29
+ it.each(['apps', '_app'])('uses basename-relative paths for the %s sub-application Settings runtime', (scope) => {
30
+ const app = new SettingsApplication({
31
+ publicPath: '/nocobase/',
32
+ router: { type: 'memory', basename: `/nocobase/settings/${scope}/demo/` },
33
+ ws: false,
34
+ });
35
+
36
+ expect(app.pluginSettingsManager.getRouteName('demo.index')).toBe('settings.demo.index');
37
+ expect(app.pluginSettingsManager.getRoutePath('')).toBe('/');
38
+ expect(app.pluginSettingsManager.getRoutePath('demo.index')).toBe('/demo');
39
+
40
+ app.router.add('settings', { path: '/settings' });
41
+ app.router.add('settingsDetails.workflow.canvas', { path: '/settings/workflow/workflows/:id' });
42
+ app.router.add('settingsDetails.preview', { path: '/settings-preview/:id' });
43
+ app.router.add('auth.signin', { path: '/signin' });
44
+
45
+ expect(app.router.get('settings')).toMatchObject({ path: '/' });
46
+ expect(app.router.get('settingsDetails.workflow.canvas')).toMatchObject({
47
+ path: '/workflow/workflows/:id',
48
+ authCheck: true,
49
+ });
50
+ expect(
51
+ app.router
52
+ .matchRoutes(`/nocobase/settings/${scope}/demo/workflow/workflows/1`)
53
+ ?.some((match) => match.route.id === 'settingsDetails.workflow.canvas'),
54
+ ).toBe(true);
55
+ expect(
56
+ app.router
57
+ .matchRoutes(`/nocobase/settings/${scope}/demo/settings/workflow/workflows/1`)
58
+ ?.some((match) => match.route.id === 'settingsDetails.workflow.canvas'),
59
+ ).toBe(false);
60
+ expect(app.router.get('settingsDetails.preview')).toMatchObject({ path: '/settings-preview/:id' });
61
+ expect(app.router.get('auth.signin')).toMatchObject({ path: '/signin' });
62
+ });
63
+
64
+ it('does not treat an apps segment inside the main application public path as a sub-application scope', () => {
65
+ const app = new SettingsApplication({
66
+ router: { type: 'memory', basename: '/tenant/apps/root/' },
67
+ ws: false,
68
+ });
69
+
70
+ expect(app.pluginSettingsManager.getRoutePath('')).toBe('/settings/');
71
+ expect(app.pluginSettingsManager.getRoutePath('demo.index')).toBe('/settings/demo');
72
+
73
+ app.router.add('settingsDetails.workflow.canvas', { path: '/settings/workflow/workflows/:id' });
74
+ expect(app.router.get('settingsDetails.workflow.canvas')).toMatchObject({
75
+ path: '/settings/workflow/workflows/:id',
76
+ });
77
+ });
78
+
79
+ it.each(['apps', '_app'])(
80
+ 'does not treat a scoped-looking %s suffix inside the main public path as a sub-application scope',
81
+ (scope) => {
82
+ const publicPath = `/tenant/settings/${scope}/root/`;
83
+ const app = new SettingsApplication({
84
+ publicPath,
85
+ router: { type: 'memory', basename: publicPath },
86
+ ws: false,
87
+ });
88
+
89
+ expect(app.pluginSettingsManager.getRoutePath('')).toBe('/settings/');
90
+ expect(app.pluginSettingsManager.getRoutePath('demo.index')).toBe('/settings/demo');
91
+
92
+ app.router.add('settingsDetails.workflow.canvas', { path: '/settings/workflow/workflows/:id' });
93
+ app.router.add('auth.signin', { path: '/signin' });
94
+
95
+ expect(app.router.get('settingsDetails.workflow.canvas')).toMatchObject({
96
+ path: '/settings/workflow/workflows/:id',
97
+ });
98
+ expect(app.router.get('auth.signin')).toMatchObject({ path: '/settings/signin' });
99
+ },
100
+ );
101
+
102
+ it('keeps only routes owned by the settings runtime', () => {
103
+ const app = new SettingsApplication({ router: { type: 'memory' }, ws: false });
104
+
105
+ app.router.add('settings', { path: '/settings' });
106
+ app.router.add('settings.demo', { path: 'demo' });
107
+ app.router.add('settingsDetails.workflow.canvas', { path: '/settings/workflow/workflows/:id' });
108
+ app.router.add('admin.demo', { path: '/admin/demo' });
109
+ app.router.add('public-forms', { path: '/public-forms/:key' });
110
+ app.router.add('mobile', { path: '/mobile' });
111
+
112
+ expect(app.router.has('settings')).toBe(true);
113
+ expect(app.router.has('settings.demo')).toBe(true);
114
+ expect(app.router.get('settingsDetails.workflow.canvas')).toMatchObject({
115
+ path: '/settings/workflow/workflows/:id',
116
+ authCheck: true,
117
+ });
118
+ expect(app.router.has('admin.demo')).toBe(false);
119
+ expect(app.router.has('public-forms')).toBe(false);
120
+ expect(app.router.has('mobile')).toBe(false);
121
+ expect(app.router.has('not-found')).toBe(true);
122
+ });
123
+
124
+ it('keeps hidden plugin detail pages in the normal Settings route tree', () => {
125
+ const app = new SettingsApplication({ router: { type: 'memory' }, ws: false });
126
+
127
+ app.pluginSettingsManager.addMenuItem({ key: 'public-forms', title: 'Public forms' });
128
+ app.pluginSettingsManager.addPageTabItem({
129
+ menuKey: 'public-forms',
130
+ key: ':name',
131
+ title: false,
132
+ hidden: true,
133
+ Component: () => null,
134
+ });
135
+
136
+ expect(app.pluginSettingsManager.get('public-forms.:name')).toMatchObject({
137
+ hidden: true,
138
+ path: '/settings/public-forms/:name',
139
+ });
140
+ expect(app.router.get('settings.public-forms.:name')).toMatchObject({ path: ':name' });
141
+ });
142
+
143
+ it('registers the authenticated settings shell without the admin layout', async () => {
144
+ const app = new SettingsApplication({ router: { type: 'memory' }, ws: false });
145
+ const apiMock = new MockAdapter(app.apiClient.axios);
146
+ apiMock.onGet('app:getLang').reply(200, { data: { lang: 'en-US', resources: { client: {} }, cron: {} } });
147
+ const plugin = new SettingsBuildInPlugin({ name: 'settings-buildin' }, app);
148
+
149
+ await plugin.afterAdd();
150
+ await plugin.load();
151
+ app.router.add('settingsDetails.workflow.canvas', { path: '/settings/workflow/workflows/:id' });
152
+
153
+ expect(app.router.get('settings')).toMatchObject({ path: '/settings', authCheck: true });
154
+ expect(app.router.get('settingsDetails')).toMatchObject({ path: '/settings', authCheck: true });
155
+ expect(
156
+ app.router.matchRoutes('/settings/workflow/workflows/1')?.some((match) => match.route.authCheck === true),
157
+ ).toBe(true);
158
+ expect(app.router.has('admin')).toBe(false);
159
+ expect(app.layoutManager.hasLayout('admin')).toBe(false);
160
+ expect(app.pluginSettingsManager.has('plugin-manager')).toBe(true);
161
+ expect(app.pluginSettingsManager.has('system-settings')).toBe(true);
162
+ expect(app.pluginSettingsManager.has('security')).toBe(true);
163
+ });
164
+ });
@@ -0,0 +1,71 @@
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 { Application } from '../index';
11
+ import { SettingsApplication } from '../settings-app/SettingsApplication';
12
+
13
+ describe('standalone settings authentication routes', () => {
14
+ it('keeps and rebases the auth and 2fa route families', () => {
15
+ const app = new SettingsApplication({ router: { type: 'memory' }, ws: false });
16
+
17
+ app.router.add('auth', { Component: () => null });
18
+ app.router.add('auth.signin', { path: '/signin', skipAuthCheck: true, Component: () => null });
19
+ app.router.add('auth.signup', { path: '/signup', skipAuthCheck: true, Component: () => null });
20
+ app.router.add('auth.forgotPassword', {
21
+ path: '/forgot-password',
22
+ skipAuthCheck: true,
23
+ Component: () => null,
24
+ });
25
+ app.router.add('auth.resetPassword', {
26
+ path: '/reset-password',
27
+ skipAuthCheck: true,
28
+ Component: () => null,
29
+ });
30
+ app.router.add('2fa', { Component: () => null });
31
+ app.router.add('2fa.verify', { path: '/2fa', skipAuthCheck: true, Component: () => null });
32
+
33
+ expect(app.router.get('auth.signin')).toMatchObject({ path: '/settings/signin', skipAuthCheck: true });
34
+ expect(app.router.get('auth.signup')).toMatchObject({ path: '/settings/signup', skipAuthCheck: true });
35
+ expect(app.router.get('auth.forgotPassword')).toMatchObject({
36
+ path: '/settings/forgot-password',
37
+ skipAuthCheck: true,
38
+ });
39
+ expect(app.router.get('auth.resetPassword')).toMatchObject({
40
+ path: '/settings/reset-password',
41
+ skipAuthCheck: true,
42
+ });
43
+ expect(app.router.get('2fa.verify')).toMatchObject({ path: '/settings/2fa', skipAuthCheck: true });
44
+ expect(app.router.matchRoutes('/settings/signin')?.map((match) => match.route.id)).toContain('auth.signin');
45
+ expect(app.router.isSkippedAuthCheckRoute('/settings/reset-password')).toBe(true);
46
+ });
47
+
48
+ it('does not change the default Client V2 authentication routes', () => {
49
+ const app = new Application({ router: { type: 'memory' }, ws: false });
50
+
51
+ app.router.add('auth', { Component: () => null });
52
+ app.router.add('auth.signin', { path: '/signin', skipAuthCheck: true, Component: () => null });
53
+ app.router.add('2fa', { Component: () => null });
54
+ app.router.add('2fa.verify', { path: '/2fa', skipAuthCheck: true, Component: () => null });
55
+
56
+ expect(app.router.get('auth.signin')).toMatchObject({ path: '/signin', skipAuthCheck: true });
57
+ expect(app.router.get('2fa.verify')).toMatchObject({ path: '/2fa', skipAuthCheck: true });
58
+ });
59
+
60
+ it('continues to reject unrelated routes from the shared plugin lane', () => {
61
+ const app = new SettingsApplication({ router: { type: 'memory' }, ws: false });
62
+
63
+ app.router.add('admin.demo', { path: '/admin/demo' });
64
+ app.router.add('public.demo', { path: '/public/demo' });
65
+ app.router.add('mobile.demo', { path: '/mobile/demo' });
66
+
67
+ expect(app.router.has('admin.demo')).toBe(false);
68
+ expect(app.router.has('public.demo')).toBe(false);
69
+ expect(app.router.has('mobile.demo')).toBe(false);
70
+ });
71
+ });