@nocobase/client-v2 2.2.0-beta.9 → 3.0.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 (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -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 { afterEach, describe, expect, it } from 'vitest';
11
+ import { getRouteRuntimeVersion } from '../utils/getRouteRuntimeVersion';
12
+
13
+ function replacePathname(pathname: string) {
14
+ Object.defineProperty(window, 'location', {
15
+ configurable: true,
16
+ value: {
17
+ ...window.location,
18
+ pathname,
19
+ },
20
+ });
21
+ }
22
+
23
+ describe('getRouteRuntimeVersion', () => {
24
+ afterEach(() => {
25
+ delete window.__nocobase_modern_client_prefix__;
26
+ delete window.__nocobase_public_path__;
27
+ replacePathname('/');
28
+ });
29
+
30
+ it('returns modern when the current pathname is inside the modern client prefix', () => {
31
+ window.__nocobase_modern_client_prefix__ = 'v';
32
+ window.__nocobase_public_path__ = '/nocobase/v/';
33
+ replacePathname('/nocobase/v/admin/settings/workflow');
34
+
35
+ expect(getRouteRuntimeVersion()).toBe('modern');
36
+ });
37
+
38
+ it('returns legacy when reused v2 components run under the v1 settings page', () => {
39
+ window.__nocobase_modern_client_prefix__ = 'v';
40
+ window.__nocobase_public_path__ = '/nocobase/';
41
+ replacePathname('/nocobase/admin/settings/workflow/workflows/123');
42
+
43
+ expect(getRouteRuntimeVersion()).toBe('legacy');
44
+ });
45
+
46
+ it('returns legacy when root public path is "/"', () => {
47
+ window.__nocobase_modern_client_prefix__ = 'v';
48
+ window.__nocobase_public_path__ = '/';
49
+ replacePathname('/admin/settings/workflow/workflows/123');
50
+
51
+ expect(getRouteRuntimeVersion()).toBe('legacy');
52
+ });
53
+
54
+ it('returns modern when root public path is "/v/"', () => {
55
+ window.__nocobase_modern_client_prefix__ = 'v';
56
+ window.__nocobase_public_path__ = '/v/';
57
+ replacePathname('/v/admin/workflow/workflows/123');
58
+
59
+ expect(getRouteRuntimeVersion()).toBe('modern');
60
+ });
61
+
62
+ it('falls back to pathname matching when public path is unavailable', () => {
63
+ window.__nocobase_modern_client_prefix__ = 'v';
64
+ replacePathname('/v/admin/workflow/workflows/123');
65
+
66
+ expect(getRouteRuntimeVersion()).toBe('modern');
67
+ });
68
+ });
@@ -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,10 +51,32 @@ 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
 
57
78
  beforeEach(() => {
79
+ globalThis.window.localStorage.clear();
58
80
  // These fixtures mount the modern client under the `v2` segment; tell the
59
81
  // runtime-prefix helper so v2-runtime detection matches (server injects it
60
82
  // in production).
@@ -75,6 +97,7 @@ describe('nocobase buildin plugin auth redirect', () => {
75
97
  });
76
98
 
77
99
  afterEach(() => {
100
+ globalThis.window.localStorage.clear();
78
101
  delete (globalThis.window as any).__nocobase_modern_client_prefix__;
79
102
  Object.defineProperty(globalThis.window, 'location', {
80
103
  configurable: true,
@@ -83,6 +106,16 @@ describe('nocobase buildin plugin auth redirect', () => {
83
106
  vi.restoreAllMocks();
84
107
  });
85
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
+
86
119
  it('should navigate to v2 signin when /auth:check returns no user', async () => {
87
120
  // Aligns with v1: use react-router navigate (virtual) rather than
88
121
  // `window.location.replace`, so a `window.location.href` queued elsewhere
@@ -90,7 +123,7 @@ describe('nocobase buildin plugin auth redirect', () => {
90
123
  // overridden.
91
124
  const app = createMockClient({
92
125
  publicPath: '/v2/',
93
- plugins: [NocoBaseBuildInPlugin as any],
126
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
94
127
  router: { type: 'memory', initialEntries: ['/v2/admin/7vu4c2sdk6h'] },
95
128
  });
96
129
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -113,9 +146,10 @@ describe('nocobase buildin plugin auth redirect', () => {
113
146
  // and race the 2FA response interceptor with its own signin redirect.
114
147
  const app = createMockClient({
115
148
  publicPath: '/v2/',
116
- plugins: [NocoBaseBuildInPlugin as any],
149
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
117
150
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
118
151
  });
152
+ app.apiClient.auth.setToken('test-token');
119
153
  app.apiMock.onGet('app:getLang').reply(200, {
120
154
  data: { lang: 'en-US', resources: { client: {} }, cron: {} },
121
155
  });
@@ -130,31 +164,37 @@ describe('nocobase buildin plugin auth redirect', () => {
130
164
  await new Promise((resolve) => setTimeout(resolve, 50));
131
165
  expect(app.router.router.state.location.pathname).toBe('/v2/admin');
132
166
  expect(app.router.router.state.location.search).toBe('');
167
+ expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(0);
133
168
  });
134
169
 
135
- 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 () => {
136
171
  const app = createMockClient({
137
172
  publicPath: '/nocobase/v2/',
138
- plugins: [NocoBaseBuildInPlugin as any],
173
+ plugins: [NocoBaseBuildInPlugin as any, TestRootPortalPlugin as typeof Plugin],
139
174
  router: { type: 'memory', initialEntries: ['/nocobase/v2/'] },
140
175
  });
141
176
  app.apiMock.onGet('app:getLang').reply(200, {
142
177
  data: { lang: 'en-US', resources: { client: {} }, cron: {} },
143
178
  });
179
+ app.apiMock.onGet('/auth:check').reply(200, { data: {} });
144
180
 
145
181
  const Root = app.getRootComponent();
146
182
  render(<Root />);
147
183
 
148
184
  await waitFor(() => {
149
185
  expect(app.router.router.state.location.pathname).toBe('/nocobase/v2/signin');
150
- expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2%2Fadmin');
186
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2');
151
187
  });
152
188
  });
153
189
 
154
190
  it('should check current user after navigating from skipped route to v2 admin', async () => {
155
191
  const app = createMockClient({
156
192
  publicPath: '/v2/',
157
- plugins: [NocoBaseBuildInPlugin as any, SkippedPublicRoutePlugin as any],
193
+ plugins: [
194
+ NocoBaseBuildInPlugin as any,
195
+ TestAdminPortalPlugin as typeof Plugin,
196
+ SkippedPublicRoutePlugin as typeof Plugin,
197
+ ],
158
198
  router: { type: 'memory', initialEntries: ['/v2/public'] },
159
199
  });
160
200
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -326,6 +366,29 @@ describe('nocobase buildin plugin auth redirect', () => {
326
366
  data: { lang: 'en-US', resources: { client: {} }, cron: {} },
327
367
  });
328
368
  app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
369
+ app.apiMock.onPost('auth:syncCookies').reply(200, { data: { synced: true } });
370
+ const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
371
+
372
+ const Root = app.getRootComponent();
373
+ render(<Root />);
374
+
375
+ expect(await screen.findByText('secure page')).toBeInTheDocument();
376
+ expect(ensureLoaded).toHaveBeenCalledTimes(1);
377
+ expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(1);
378
+ });
379
+
380
+ it('should keep authenticated v2 startup working when cookie bootstrap fails', async () => {
381
+ const app = createMockClient({
382
+ publicPath: '/v2/',
383
+ plugins: [NocoBaseBuildInPlugin as any, AuthBootstrapRoutePlugin as any],
384
+ router: { type: 'memory', initialEntries: ['/v2/secure'] },
385
+ });
386
+ app.apiClient.auth.setToken('test-token');
387
+ app.apiMock.onGet('app:getLang').reply(200, {
388
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
389
+ });
390
+ app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
391
+ app.apiMock.onPost('auth:syncCookies').reply(500, { errors: [{ message: 'cookie sync failed' }] });
329
392
  const ensureLoaded = vi.spyOn(app.dataSourceManager, 'ensureLoaded').mockResolvedValue(undefined);
330
393
 
331
394
  const Root = app.getRootComponent();
@@ -333,6 +396,7 @@ describe('nocobase buildin plugin auth redirect', () => {
333
396
 
334
397
  expect(await screen.findByText('secure page')).toBeInTheDocument();
335
398
  expect(ensureLoaded).toHaveBeenCalledTimes(1);
399
+ expect(app.apiMock.history.post.filter((request) => request.url === 'auth:syncCookies')).toHaveLength(1);
336
400
  });
337
401
 
338
402
  it('should not auth-check or bootstrap authCheck false routes', async () => {
@@ -357,7 +421,7 @@ describe('nocobase buildin plugin auth redirect', () => {
357
421
  it('should render v2 admin root without redirecting away', async () => {
358
422
  const app = createMockClient({
359
423
  publicPath: '/v2/',
360
- plugins: [NocoBaseBuildInPlugin as any],
424
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
361
425
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
362
426
  });
363
427
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -387,11 +451,11 @@ describe('nocobase buildin plugin auth redirect', () => {
387
451
  });
388
452
 
389
453
  it.each(['/v2/admin/legacy-page/tab/tab-1', '/v2/admin/legacy-page/view/detail'])(
390
- 'should show 404 for authenticated direct v1-style v2 page access: %s',
454
+ 'should explain authenticated direct v1-style v2 page access: %s',
391
455
  async (pathname) => {
392
456
  const app = createMockClient({
393
457
  publicPath: '/v2/',
394
- plugins: [NocoBaseBuildInPlugin as any],
458
+ plugins: [NocoBaseBuildInPlugin as any, TestAdminPortalPlugin as typeof Plugin],
395
459
  router: { type: 'memory', initialEntries: [pathname] },
396
460
  });
397
461
  app.apiMock.onGet('app:getLang').reply(200, {
@@ -413,7 +477,12 @@ describe('nocobase buildin plugin auth redirect', () => {
413
477
  const Root = app.getRootComponent();
414
478
  render(<Root />);
415
479
 
416
- 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
+ );
417
486
  expect(app.router.router.state.location.pathname).toBe(pathname);
418
487
  },
419
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, {
@@ -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
+ });