@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
@@ -64,6 +64,7 @@ const languageCodes = {
64
64
  "ml-IN": { label: "\u0D2E\u0D32\u0D2F\u0D3E\u0D33\u0D02" },
65
65
  "mn-MN": { label: "\u041C\u043E\u043D\u0433\u043E\u043B \u0445\u044D\u043B" },
66
66
  "ms-MY": { label: "\u0628\u0647\u0627\u0633 \u0645\u0644\u0627\u064A\u0648" },
67
+ "my-MM": { label: "\u1019\u103C\u1014\u103A\u1019\u102C\u1018\u102C\u101E\u102C" },
67
68
  "nb-NO": { label: "Norsk bokm\xE5l" },
68
69
  "ne-NP": { label: "\u0928\u0947\u092A\u093E\u0932\u0940" },
69
70
  "nl-BE": { label: "Vlaams" },
@@ -83,7 +84,8 @@ const languageCodes = {
83
84
  "tk-TK": { label: "Turkmen" },
84
85
  "tr-TR": { label: "T\xFCrk\xE7e" },
85
86
  "uk-UA": { label: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
86
- "ur-PK": { label: "O\u02BBzbekcha" },
87
+ "ur-PK": { label: "\u0627\u0631\u062F\u0648" },
88
+ "uz-UZ": { label: "O\u02BBzbekcha" },
87
89
  "vi-VN": { label: "Ti\u1EBFng Vi\u1EC7t" },
88
90
  "zh-CN": { label: "\u7B80\u4F53\u4E2D\u6587" },
89
91
  "zh-HK": { label: "\u7E41\u9AD4\u4E2D\u6587\uFF08\u9999\u6E2F\uFF09" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.2.0-beta.9",
3
+ "version": "3.0.0-alpha.1",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.2.0-beta.9",
31
- "@nocobase/flow-engine": "2.2.0-beta.9",
32
- "@nocobase/sdk": "2.2.0-beta.9",
33
- "@nocobase/shared": "2.2.0-beta.9",
34
- "@nocobase/utils": "2.2.0-beta.9",
30
+ "@nocobase/evaluators": "3.0.0-alpha.1",
31
+ "@nocobase/flow-engine": "3.0.0-alpha.1",
32
+ "@nocobase/sdk": "3.0.0-alpha.1",
33
+ "@nocobase/shared": "3.0.0-alpha.1",
34
+ "@nocobase/utils": "3.0.0-alpha.1",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -42,9 +42,11 @@
42
42
  "html5-qrcode": "^2.3.8",
43
43
  "i18next": "^22.4.9",
44
44
  "json5": "^2.2.3",
45
+ "jsqr": "^1.4.0",
45
46
  "lodash": "4.17.21",
47
+ "react-device-detect": "2.2.3",
46
48
  "react-i18next": "^11.15.1",
47
49
  "react-router-dom": "^6.30.1"
48
50
  },
49
- "gitHead": "60e3d7abbaa0c7cead76f71a4f3d5eedb6b8acdb"
51
+ "gitHead": "22d8be8ece179cfa5c8a4ebb2c896c92bd418e03"
50
52
  }
package/src/APIClient.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { APIClient as APIClientSDK, type APIClientOptions, hasHeaderValue } from '@nocobase/sdk';
10
+ import { APIClient as APIClientSDK, hasHeaderValue } from '@nocobase/sdk';
11
11
 
12
12
  function offsetToTimeZone(offset: number) {
13
13
  const hours = Math.floor(Math.abs(offset));
@@ -23,15 +23,6 @@ function getCurrentTimezone() {
23
23
  }
24
24
 
25
25
  export class APIClient extends APIClientSDK {
26
- appName?: string;
27
-
28
- constructor(options?: APIClientOptions) {
29
- super(options);
30
- if (options && typeof options !== 'function') {
31
- this.appName = options.appName;
32
- }
33
- }
34
-
35
26
  getHostname() {
36
27
  if (process.env.API_BASE_URL) {
37
28
  try {
@@ -46,6 +46,10 @@ export class Application extends BaseApplication<
46
46
 
47
47
  protected createApiClient(options: ApplicationOptions) {
48
48
  return new APIClient({
49
+ // Cross-origin API deployments rely on cookies for auth (e.g. permanent file
50
+ // URLs); trust is enforced server-side via the CORS origin whitelist and the
51
+ // CSRF middleware, not by omitting credentials.
52
+ withCredentials: true,
49
53
  ...options.apiClient,
50
54
  appName: options.name || getSubAppName(options.publicPath),
51
55
  });
@@ -35,6 +35,7 @@ import { SystemSettingsSource } from './flow/system-settings';
35
35
  import { LayoutManager } from './layout-manager/LayoutManager';
36
36
  import type { PluginClass, PluginManager, PluginType } from './PluginManager';
37
37
  import { RouteRepository } from './RouteRepository';
38
+ import { stripModernClientPrefix } from './authRedirect';
38
39
  import type {
39
40
  ComponentTypeAndString,
40
41
  RenderableComponentType,
@@ -81,6 +82,10 @@ const trimTrailingSlashes = (value: string) => {
81
82
  return match ? value.slice(0, -match[0].length) : value;
82
83
  };
83
84
 
85
+ const ensureTrailingSlash = (value: string) => {
86
+ return `${trimTrailingSlashes(value)}/`;
87
+ };
88
+
84
89
  const isRenderableComponentType = (value: unknown): value is AnyComponent => isValidElementType(value);
85
90
 
86
91
  export type DevDynamicImport = (packageName: string) => Promise<{ default: PluginClass }>;
@@ -398,7 +403,7 @@ export abstract class BaseApplication<
398
403
  this.favicon = favicon || '';
399
404
  }
400
405
 
401
- const iconHref = this.favicon || '/favicon/favicon.ico';
406
+ const iconHref = this.favicon || this.getCdnUrl() + 'favicon/favicon.ico';
402
407
 
403
408
  if (faviconLinkElement) {
404
409
  faviconLinkElement.href = iconHref;
@@ -433,12 +438,12 @@ export abstract class BaseApplication<
433
438
  return getSubAppName(this.getPublicPath()) || null;
434
439
  }
435
440
 
441
+ getCdnUrl() {
442
+ return ensureTrailingSlash(window['__webpack_public_path__'] || stripModernClientPrefix(this.getPublicPath()));
443
+ }
444
+
436
445
  getPublicPath() {
437
- let publicPath = this.options.publicPath || '/';
438
- if (!publicPath.endsWith('/')) {
439
- publicPath += '/';
440
- }
441
- return publicPath;
446
+ return ensureTrailingSlash(this.options.publicPath || '/');
442
447
  }
443
448
 
444
449
  getApiUrl(pathname = '') {
@@ -34,6 +34,8 @@ describe('app', () => {
34
34
  afterEach(() => {
35
35
  document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
36
36
  document.documentElement.removeAttribute('lang');
37
+ delete window['__webpack_public_path__'];
38
+ delete window['__nocobase_modern_client_prefix__'];
37
39
  vi.restoreAllMocks();
38
40
  });
39
41
 
@@ -50,6 +52,24 @@ describe('app', () => {
50
52
  expect(app.getHref('/test')).toBe('/test');
51
53
  });
52
54
 
55
+ it('should enable credentials for api clients by default', () => {
56
+ const sameOriginApp = new Application({
57
+ router,
58
+ apiClient: {
59
+ baseURL: '/api/',
60
+ },
61
+ });
62
+ const crossOriginApp = new Application({
63
+ router,
64
+ apiClient: {
65
+ baseURL: 'https://api.example.com/api/',
66
+ },
67
+ });
68
+
69
+ expect(sameOriginApp.apiClient.axios.defaults.withCredentials).toBe(true);
70
+ expect(crossOriginApp.apiClient.axios.defaults.withCredentials).toBe(true);
71
+ });
72
+
53
73
  it('should initialize shared jsonLogic operators', () => {
54
74
  const app = new Application({ router });
55
75
 
@@ -58,6 +78,59 @@ describe('app', () => {
58
78
  expect(app.jsonLogic.apply({ $testAlwaysTrue: [] })).toBe(true);
59
79
  });
60
80
 
81
+ it('should normalize publicPath and webpack public path with a single trailing slash', () => {
82
+ const app = new Application({
83
+ router,
84
+ publicPath: '/admin//',
85
+ });
86
+
87
+ expect(app.getPublicPath()).toBe('/admin/');
88
+
89
+ window['__webpack_public_path__'] = '/cdn/assets///';
90
+ expect(app.getCdnUrl()).toBe('/cdn/assets/');
91
+
92
+ delete window['__webpack_public_path__'];
93
+ expect(app.getCdnUrl()).toBe('/admin/');
94
+ });
95
+
96
+ it('should normalize webpack public path without a trailing slash', () => {
97
+ const app = new Application({
98
+ router,
99
+ publicPath: '/admin/',
100
+ });
101
+
102
+ window['__webpack_public_path__'] = '/cdn/assets';
103
+ expect(app.getCdnUrl()).toBe('/cdn/assets/');
104
+ });
105
+
106
+ it('should remove the modern client prefix from the CDN fallback path', () => {
107
+ const app = new Application({
108
+ router,
109
+ publicPath: '/v/',
110
+ });
111
+
112
+ expect(app.getCdnUrl()).toBe('/');
113
+ });
114
+
115
+ it('should preserve APP_PUBLIC_PATH when removing the modern client prefix', () => {
116
+ const app = new Application({
117
+ router,
118
+ publicPath: '/nocobase/v/',
119
+ });
120
+
121
+ expect(app.getCdnUrl()).toBe('/nocobase/');
122
+ });
123
+
124
+ it('should support a custom modern client prefix', () => {
125
+ window['__nocobase_modern_client_prefix__'] = 'modern';
126
+ const app = new Application({
127
+ router,
128
+ publicPath: '/nocobase/modern/',
129
+ });
130
+
131
+ expect(app.getCdnUrl()).toBe('/nocobase/');
132
+ });
133
+
61
134
  it('should apply the provided favicon immediately', () => {
62
135
  const app = new Application({ router });
63
136
 
@@ -17,12 +17,18 @@ import {
17
17
 
18
18
  describe('auth redirect helpers', () => {
19
19
  const originalLocation = globalThis.window.location;
20
+ const originalModernClientPrefix = window.__nocobase_modern_client_prefix__;
20
21
 
21
22
  afterEach(() => {
22
23
  Object.defineProperty(globalThis.window, 'location', {
23
24
  configurable: true,
24
25
  value: originalLocation,
25
26
  });
27
+ if (originalModernClientPrefix === undefined) {
28
+ delete window.__nocobase_modern_client_prefix__;
29
+ } else {
30
+ window.__nocobase_modern_client_prefix__ = originalModernClientPrefix;
31
+ }
26
32
  vi.restoreAllMocks();
27
33
  });
28
34
 
@@ -84,6 +90,107 @@ describe('auth redirect helpers', () => {
84
90
  expect(buildV2SigninHref(app, '/v2/admin/7vu4c2sdk6h')).toBe('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
85
91
  });
86
92
 
93
+ it('should keep validated standalone settings redirects outside the v2 basename', () => {
94
+ const app = {
95
+ getPublicPath: () => '/nocobase/v2/',
96
+ router: {
97
+ getBasename: () => '/nocobase/v2',
98
+ },
99
+ } as any;
100
+
101
+ expect(normalizeV2RedirectPath(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
102
+ '/nocobase/settings/workflow?tab=list#recent',
103
+ );
104
+ });
105
+
106
+ it('should build the standalone settings signin URL for a standalone settings app', () => {
107
+ const app = {
108
+ name: 'main',
109
+ getPublicPath: () => '/nocobase/',
110
+ router: {
111
+ getBasename: () => '/nocobase',
112
+ },
113
+ pluginSettingsManager: {
114
+ getRouteName: () => 'settings.',
115
+ getRoutePath: () => '/settings/',
116
+ },
117
+ } satisfies Parameters<typeof buildV2SigninHref>[0];
118
+ expect(buildV2SigninHref(app, '/nocobase/settings/workflow?tab=list#recent')).toBe(
119
+ '/nocobase/settings/signin?redirect=%2Fnocobase%2Fsettings%2Fworkflow%3Ftab%3Dlist%23recent',
120
+ );
121
+ });
122
+
123
+ it('should keep an apps segment inside the main public path out of the Settings application scope', () => {
124
+ const app = {
125
+ name: 'main',
126
+ getPublicPath: () => '/tenant/apps/root/',
127
+ router: {
128
+ getBasename: () => '/tenant/apps/root/',
129
+ },
130
+ pluginSettingsManager: {
131
+ getRouteName: () => 'settings.',
132
+ getRoutePath: () => '/settings/',
133
+ },
134
+ } satisfies Parameters<typeof buildV2SigninHref>[0];
135
+
136
+ expect(buildV2SigninHref(app, '/tenant/apps/root/settings/workflow')).toBe(
137
+ '/tenant/apps/root/settings/signin?redirect=%2Ftenant%2Fapps%2Froot%2Fsettings%2Fworkflow',
138
+ );
139
+ });
140
+
141
+ it.each([
142
+ ['/nocobase/settings/apps/test-app/', '/nocobase/settings/apps/test-app/signin'],
143
+ ['/nocobase/settings/_app/test-app/', '/nocobase/settings/_app/test-app/signin'],
144
+ ])('should keep the standalone settings signin in the current sub-app scope: %s', (basename, signinPath) => {
145
+ const app = {
146
+ name: 'test-app',
147
+ getPublicPath: () => '/nocobase/',
148
+ router: {
149
+ getBasename: () => basename,
150
+ },
151
+ pluginSettingsManager: {
152
+ getRouteName: () => 'settings.',
153
+ getRoutePath: () => '/',
154
+ },
155
+ } as any;
156
+
157
+ expect(buildV2SigninHref(app, `${basename}workflow`)).toBe(
158
+ `${signinPath}?redirect=${encodeURIComponent(`${basename}workflow`)}`,
159
+ );
160
+ });
161
+
162
+ it('should accept the current standalone settings signin URL only', () => {
163
+ Object.defineProperty(globalThis.window, 'location', {
164
+ configurable: true,
165
+ value: {
166
+ ...originalLocation,
167
+ origin: 'http://localhost:20000',
168
+ },
169
+ });
170
+ const app = {
171
+ name: 'test-app',
172
+ getPublicPath: () => '/nocobase/',
173
+ router: {
174
+ getBasename: () => '/nocobase/settings/apps/test-app/',
175
+ },
176
+ pluginSettingsManager: {
177
+ getRouteName: () => 'settings.',
178
+ getRoutePath: () => '/',
179
+ },
180
+ } as any;
181
+
182
+ expect(
183
+ resolveV2SigninRedirect(
184
+ '/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
185
+ app,
186
+ ),
187
+ ).toBe(
188
+ 'http://localhost:20000/nocobase/settings/apps/test-app/signin?redirect=%2Fnocobase%2Fsettings%2Fapps%2Ftest-app',
189
+ );
190
+ expect(resolveV2SigninRedirect('/nocobase/v/apps/test-app/signin', app)).toBeNull();
191
+ expect(resolveV2SigninRedirect('/nocobase/settings/apps/other-app/signin', app)).toBeNull();
192
+ });
193
+
87
194
  it('should redirect with window.location.replace by default', () => {
88
195
  const replace = vi.fn();
89
196
  Object.defineProperty(globalThis.window, 'location', {
@@ -134,6 +241,36 @@ describe('auth redirect helpers', () => {
134
241
  });
135
242
 
136
243
  describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
244
+ it('should keep only the current sub-app standalone settings redirect', () => {
245
+ const app = {
246
+ getPublicPath: () => '/nocobase/v2/',
247
+ router: {
248
+ getBasename: () => '/nocobase/v2/apps/test-app/',
249
+ },
250
+ } as any;
251
+
252
+ expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/test-app/workflow')).toBe(
253
+ '/nocobase/settings/apps/test-app/workflow',
254
+ );
255
+ expect(normalizeV2RedirectPath(app, '/nocobase/settings/apps/other-app/workflow')).toBe(
256
+ '/nocobase/v2/apps/test-app/admin/',
257
+ );
258
+ });
259
+
260
+ it.each([
261
+ '/nocobase/settings/apps/test-app/../../other-app/settings',
262
+ '/nocobase/settings/apps/test-app/%2e%2e/%2E%2e/other-app/settings',
263
+ ])('should reject a standalone settings redirect that resolves outside the current sub-app: %s', (target) => {
264
+ const app = {
265
+ getPublicPath: () => '/nocobase/v2/',
266
+ router: {
267
+ getBasename: () => '/nocobase/v2/apps/test-app/',
268
+ },
269
+ } as any;
270
+
271
+ expect(normalizeV2RedirectPath(app, target)).toBe('/nocobase/v2/apps/test-app/admin/');
272
+ });
273
+
137
274
  it('should normalize signin redirect fallback under the current sub-app basename', () => {
138
275
  const app = {
139
276
  getPublicPath: () => '/v/',
@@ -15,6 +15,11 @@ import { fileURLToPath } from 'node:url';
15
15
  type BrowserCheckerCase = {
16
16
  pathname: string;
17
17
  publicPath: string;
18
+ search?: string;
19
+ hash?: string;
20
+ modernClientPrefix?: string;
21
+ appClientEntryMode?: string;
22
+ scriptSrc?: string;
18
23
  expectedRedirect?: string;
19
24
  };
20
25
 
@@ -40,11 +45,13 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
40
45
  showLog: false,
41
46
  window: {
42
47
  __nocobase_public_path__: input.publicPath,
48
+ __nocobase_modern_client_prefix__: input.modernClientPrefix,
49
+ __nocobase_app_client_entry_mode__: input.appClientEntryMode,
43
50
  location: {
44
51
  origin: 'http://c.local.nocobase.com',
45
52
  pathname: input.pathname,
46
- search: '',
47
- hash: '',
53
+ search: input.search || '',
54
+ hash: input.hash || '',
48
55
  replace,
49
56
  },
50
57
  console: consoleMock,
@@ -54,6 +61,13 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
54
61
  onresize: undefined,
55
62
  },
56
63
  document: {
64
+ currentScript: {
65
+ src:
66
+ input.scriptSrc ||
67
+ (scriptPath.includes('/app/client-v2/public/')
68
+ ? 'http://assets.local.nocobase.com/v/browser-checker.js?v=1'
69
+ : 'http://assets.local.nocobase.com/browser-checker.js?v=1'),
70
+ },
57
71
  documentElement: {
58
72
  className: '',
59
73
  clientWidth: 1280,
@@ -76,11 +90,11 @@ function executeBrowserChecker(scriptPath: string, input: BrowserCheckerCase) {
76
90
  describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
77
91
  it('normalizes a relative public path before redirecting to the trailing-slash entry', () => {
78
92
  const replace = executeBrowserChecker(scriptPath, {
79
- pathname: '/nocobase/v',
80
- publicPath: 'nocobase/v/',
93
+ pathname: '/nocobase/console',
94
+ publicPath: 'nocobase/console/',
81
95
  });
82
96
 
83
- expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
97
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/console/');
84
98
  });
85
99
 
86
100
  it('prefixes outside paths with a root-relative basename instead of duplicating a relative segment', () => {
@@ -94,7 +108,7 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
94
108
 
95
109
  it('does not redirect when the current path is already under the normalized basename', () => {
96
110
  const replace = executeBrowserChecker(scriptPath, {
97
- pathname: '/nocobase/v/',
111
+ pathname: '/nocobase/v/admin',
98
112
  publicPath: 'nocobase/v/',
99
113
  });
100
114
 
@@ -109,4 +123,169 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
109
123
 
110
124
  expect(replace).not.toHaveBeenCalled();
111
125
  });
126
+
127
+ if (scriptPath.includes('/app/client/public/')) {
128
+ it('redirects app root to modern entry for modern-default', () => {
129
+ const replace = executeBrowserChecker(scriptPath, {
130
+ pathname: '/',
131
+ publicPath: '/',
132
+ modernClientPrefix: 'v',
133
+ appClientEntryMode: 'modern-default',
134
+ });
135
+
136
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/');
137
+ });
138
+
139
+ it('does not redirect legacy deep links for modern-default', () => {
140
+ const replace = executeBrowserChecker(scriptPath, {
141
+ pathname: '/admin',
142
+ publicPath: '/',
143
+ modernClientPrefix: 'v',
144
+ appClientEntryMode: 'modern-default',
145
+ });
146
+
147
+ expect(replace).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it('rewrites legacy document paths for modern-only', () => {
151
+ const replace = executeBrowserChecker(scriptPath, {
152
+ pathname: '/admin/settings/workflow',
153
+ publicPath: '/',
154
+ modernClientPrefix: 'v',
155
+ appClientEntryMode: 'modern-only',
156
+ });
157
+
158
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/v/admin/settings/workflow');
159
+ });
160
+
161
+ it('redirects sub-path site root directly to final modern target', () => {
162
+ const replace = executeBrowserChecker(scriptPath, {
163
+ pathname: '/nocobase/',
164
+ publicPath: '/nocobase/',
165
+ modernClientPrefix: 'v',
166
+ appClientEntryMode: 'modern-default',
167
+ });
168
+
169
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/');
170
+ });
171
+
172
+ it('rewrites sub-app legacy deep links for modern-only without collapsing the sub-app segment', () => {
173
+ const replace = executeBrowserChecker(scriptPath, {
174
+ pathname: '/nocobase/apps/a_31itq60q4kg/admin/',
175
+ publicPath: '/nocobase/',
176
+ modernClientPrefix: 'v',
177
+ appClientEntryMode: 'modern-only',
178
+ });
179
+
180
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/nocobase/v/apps/a_31itq60q4kg/admin');
181
+ });
182
+ }
183
+
184
+ if (scriptPath.includes('/app/client-v2/public/')) {
185
+ it.each(['/v', '/v/'])('redirects the main modern root %s to Settings', (pathname) => {
186
+ const replace = executeBrowserChecker(scriptPath, {
187
+ pathname,
188
+ publicPath: '/v/',
189
+ modernClientPrefix: 'v',
190
+ });
191
+
192
+ expect(replace).toHaveBeenCalledOnce();
193
+ expect(replace).toHaveBeenCalledWith('http://c.local.nocobase.com/settings');
194
+ });
195
+
196
+ it.each([
197
+ ['/v/apps/demo', '/settings/apps/demo'],
198
+ ['/v/apps/demo/', '/settings/apps/demo'],
199
+ ['/v/_app/demo', '/settings/_app/demo'],
200
+ ['/v/_app/demo/', '/settings/_app/demo'],
201
+ ])('redirects the scoped modern root %s to %s', (pathname, expectedPathname) => {
202
+ const replace = executeBrowserChecker(scriptPath, {
203
+ pathname,
204
+ publicPath: '/v/',
205
+ modernClientPrefix: 'v',
206
+ });
207
+
208
+ expect(replace).toHaveBeenCalledOnce();
209
+ expect(replace).toHaveBeenCalledWith(`http://c.local.nocobase.com${expectedPathname}`);
210
+ });
211
+
212
+ it.each([
213
+ ['/settings/apps/demo', '/', '/settings/apps/demo/', 'v'],
214
+ ['/settings/_app/demo', '/', '/settings/_app/demo/', 'v'],
215
+ ['/nocobase/settings/apps/demo', '/nocobase/', '/nocobase/settings/apps/demo/', 'v'],
216
+ ['/nocobase/settings/_app/demo', '/nocobase/', '/nocobase/settings/_app/demo/', 'v'],
217
+ ['/tenant/apps/root/settings/apps/demo', '/tenant/apps/root/', '/tenant/apps/root/settings/apps/demo/', 'v'],
218
+ ['/tenant/_app/root/settings/_app/demo', '/tenant/_app/root/', '/tenant/_app/root/settings/_app/demo/', 'v'],
219
+ ['/tenant/v/settings/apps/demo', '/tenant/v/', '/tenant/v/settings/apps/demo/', 'v'],
220
+ ['/tenant/modern/settings/_app/demo', '/tenant/modern/', '/tenant/modern/settings/_app/demo/', 'modern'],
221
+ ])('normalizes the scoped Settings root %s to %s', (pathname, publicPath, expectedPathname, modernClientPrefix) => {
222
+ const replace = executeBrowserChecker(scriptPath, {
223
+ pathname,
224
+ publicPath,
225
+ modernClientPrefix,
226
+ scriptSrc: 'https://cdn.example.com/ui/settings/browser-checker.js?v=1',
227
+ search: '?tab=overview',
228
+ hash: '#panel',
229
+ });
230
+
231
+ expect(replace).toHaveBeenCalledOnce();
232
+ expect(replace).toHaveBeenCalledWith(`http://c.local.nocobase.com${expectedPathname}?tab=overview#panel`);
233
+ });
234
+
235
+ it.each([
236
+ ['/tenant/apps/root/modern/apps/demo/', '/tenant/apps/root/modern/', '/tenant/apps/root/settings/apps/demo'],
237
+ ['/tenant/_app/root/modern/_app/demo/', '/tenant/_app/root/modern/', '/tenant/_app/root/settings/_app/demo'],
238
+ ])(
239
+ 'preserves public path, query, and hash for %s without treating public-path segments as app scope',
240
+ (pathname, publicPath, expectedPathname) => {
241
+ const replace = executeBrowserChecker(scriptPath, {
242
+ pathname,
243
+ publicPath,
244
+ modernClientPrefix: 'modern',
245
+ search: '?tab=overview',
246
+ hash: '#panel',
247
+ });
248
+
249
+ expect(replace).toHaveBeenCalledOnce();
250
+ expect(replace).toHaveBeenCalledWith(`http://c.local.nocobase.com${expectedPathname}?tab=overview#panel`);
251
+ },
252
+ );
253
+
254
+ it.each([
255
+ ['/v/admin', '/v/'],
256
+ ['/v/apps/demo/admin', '/v/'],
257
+ ['/v/_app/demo/admin', '/v/'],
258
+ ['/v/_apps/demo', '/v/'],
259
+ ['/v/settings/apps/demo', '/v/'],
260
+ ['/nocobase/modern/settings/_app/demo', '/nocobase/modern/', 'modern'],
261
+ ])('does not redirect a non-root modern path %s to Settings', (pathname, publicPath, modernClientPrefix = 'v') => {
262
+ const replace = executeBrowserChecker(scriptPath, {
263
+ pathname,
264
+ publicPath,
265
+ modernClientPrefix,
266
+ });
267
+
268
+ expect(replace).not.toHaveBeenCalled();
269
+ });
270
+
271
+ it.each([
272
+ ['/settings', '/'],
273
+ ['/settings/', '/'],
274
+ ['/settings/apps/demo/', '/'],
275
+ ['/settings/_app/demo/', '/'],
276
+ ['/settings/apps/demo/multi-portal', '/'],
277
+ ['/settings/_app/demo/multi-portal', '/'],
278
+ ['/settings/_apps/demo', '/'],
279
+ ['/nocobase/settings/apps/demo/multi-portal', '/nocobase/'],
280
+ ])('does not redirect a non-target Settings path %s', (pathname, publicPath) => {
281
+ const replace = executeBrowserChecker(scriptPath, {
282
+ pathname,
283
+ publicPath,
284
+ modernClientPrefix: 'v',
285
+ scriptSrc: 'https://cdn.example.com/ui/settings/browser-checker.js?v=1',
286
+ });
287
+
288
+ expect(replace).not.toHaveBeenCalled();
289
+ });
290
+ }
112
291
  });