@nocobase/client-v2 2.2.0-beta.8 → 2.2.0

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 (271) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouteRepository.d.ts +8 -0
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -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 +1 -0
  18. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  19. package/es/entry-actions/index.d.ts +9 -0
  20. package/es/flow/actions/index.d.ts +1 -1
  21. package/es/flow/actions/linkageRules.d.ts +27 -0
  22. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  23. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  25. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  26. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  27. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  28. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  29. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  30. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  31. package/es/flow/components/FlowRoute.d.ts +2 -2
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  34. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  35. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  36. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  37. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  38. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  39. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  40. package/es/flow/index.d.ts +1 -0
  41. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  42. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  43. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  44. package/es/flow/models/base/ActionModelCore.d.ts +6 -2
  45. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  46. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  47. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  48. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  49. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  50. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  51. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  52. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  53. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  54. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  55. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  56. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  57. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  58. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  59. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  60. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  61. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  62. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  63. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  64. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  65. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  66. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  67. package/es/flow-compat/passwordUtils.d.ts +1 -1
  68. package/es/flow-compat/routeTypes.d.ts +1 -0
  69. package/es/index.d.ts +7 -0
  70. package/es/index.mjs +301 -177
  71. package/es/ui-operation/index.d.ts +15 -0
  72. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  73. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  74. package/es/utils/index.d.ts +1 -0
  75. package/es/utils/markdownSanitize.d.ts +11 -0
  76. package/lib/index.js +313 -189
  77. package/lib/locale/languageCodes.js +3 -1
  78. package/package.json +9 -7
  79. package/src/APIClient.ts +90 -7
  80. package/src/Application.tsx +7 -1
  81. package/src/BaseApplication.tsx +13 -6
  82. package/src/RouteRepository.ts +25 -0
  83. package/src/RouterManager.tsx +142 -1
  84. package/src/__tests__/APIClient.test.tsx +58 -0
  85. package/src/__tests__/RouteRepository.test.ts +23 -0
  86. package/src/__tests__/RouterManager.test.ts +125 -0
  87. package/src/__tests__/app.test.tsx +73 -0
  88. package/src/__tests__/authRedirect.test.ts +17 -0
  89. package/src/__tests__/browserChecker.test.ts +61 -0
  90. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  91. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  92. package/src/__tests__/settings-center.test.tsx +270 -6
  93. package/src/acl/ACLProvider.tsx +2 -0
  94. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  95. package/src/authRedirect.ts +38 -0
  96. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  97. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  98. package/src/collection-manager/field-validation.ts +1 -1
  99. package/src/components/README.md +7 -1
  100. package/src/components/README.zh-CN.md +6 -1
  101. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  102. package/src/components/category-tabs/index.ts +10 -0
  103. package/src/components/form/JsonTextArea.tsx +4 -0
  104. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  105. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  106. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  107. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  108. package/src/components/form/TypedVariableInput.tsx +452 -101
  109. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  110. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  111. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  112. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  113. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  114. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  115. package/src/components/form/filter/index.ts +2 -0
  116. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  117. package/src/components/form/table/Table.tsx +81 -6
  118. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  119. package/src/components/index.ts +1 -0
  120. package/src/entry-actions/EntryActionManager.ts +76 -0
  121. package/src/entry-actions/index.ts +10 -0
  122. package/src/flow/FlowPage.tsx +38 -3
  123. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  124. package/src/flow/__tests__/FlowRoute.test.tsx +675 -12
  125. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  126. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  127. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  128. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  129. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  130. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  131. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  132. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  133. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  134. package/src/flow/actions/dataScopeFilter.ts +10 -1
  135. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  136. package/src/flow/actions/index.ts +2 -0
  137. package/src/flow/actions/linkageRules.tsx +144 -26
  138. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  139. package/src/flow/actions/openView.tsx +21 -1
  140. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  141. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  142. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  143. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
  144. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  145. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  146. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  147. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  148. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  149. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  150. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  151. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  152. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  153. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  154. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  155. package/src/flow/common/Markdown/Display.tsx +14 -3
  156. package/src/flow/common/Markdown/Edit.tsx +19 -7
  157. package/src/flow/common/Markdown/style.ts +4 -0
  158. package/src/flow/components/BlockItemCard.tsx +2 -2
  159. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  160. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  161. package/src/flow/components/FlowRoute.tsx +204 -46
  162. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  163. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  164. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  165. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  166. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  167. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  168. package/src/flow/components/field-value-variable/index.ts +12 -0
  169. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  170. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  171. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  172. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  173. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  174. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  175. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  176. package/src/flow/index.ts +3 -0
  177. package/src/flow/internal/components/Markdown/util.ts +2 -1
  178. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  179. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  180. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  181. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  182. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  183. package/src/flow/models/base/ActionModel.tsx +10 -8
  184. package/src/flow/models/base/ActionModelCore.tsx +16 -2
  185. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  186. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  187. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  188. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  189. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  190. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  191. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  192. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  193. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  194. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  195. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  196. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  197. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  198. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  199. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  200. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  201. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  202. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  203. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  204. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  205. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  206. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  207. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  208. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  209. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  210. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  211. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  212. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  213. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  214. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  215. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  216. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  217. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  218. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  219. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  220. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  221. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  222. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  223. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  224. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  225. package/src/flow/models/blocks/table/TableColumnModel.tsx +54 -11
  226. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  227. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  228. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  229. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +84 -0
  230. package/src/flow/models/blocks/table/utils.ts +7 -0
  231. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  232. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  233. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  234. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  235. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  236. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +54 -149
  237. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  238. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  239. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  240. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  241. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  242. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  243. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  245. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  246. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  247. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  248. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  249. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  250. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  251. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  252. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  253. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  254. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  255. package/src/flow/routeTransientInputArgs.ts +27 -0
  256. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  257. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  258. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  259. package/src/flow-compat/routeTypes.ts +1 -0
  260. package/src/index.ts +7 -0
  261. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  262. package/src/layout-manager/LayoutRoute.tsx +2 -1
  263. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  264. package/src/locale/languageCodes.ts +3 -1
  265. package/src/nocobase-buildin-plugin/index.tsx +19 -1
  266. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  267. package/src/ui-operation/index.ts +33 -0
  268. package/src/ui-operation/ui-operation-codec.ts +54 -0
  269. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  270. package/src/utils/index.tsx +1 -0
  271. package/src/utils/markdownSanitize.ts +88 -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.8",
3
+ "version": "2.2.0",
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.8",
31
- "@nocobase/flow-engine": "2.2.0-beta.8",
32
- "@nocobase/sdk": "2.2.0-beta.8",
33
- "@nocobase/shared": "2.2.0-beta.8",
34
- "@nocobase/utils": "2.2.0-beta.8",
30
+ "@nocobase/evaluators": "2.2.0",
31
+ "@nocobase/flow-engine": "2.2.0",
32
+ "@nocobase/sdk": "2.2.0",
33
+ "@nocobase/shared": "2.2.0",
34
+ "@nocobase/utils": "2.2.0",
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": "fa2502c1e9faf6d74b3f51b42dbc6546638d46af"
51
+ "gitHead": "d5052cae37177054c236f1bf7bba3aed6ef029d0"
50
52
  }
package/src/APIClient.ts CHANGED
@@ -7,7 +7,69 @@
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
+ import type { NotificationInstance } from 'antd/es/notification/interface';
12
+ import type { AxiosRequestConfig } from 'axios';
13
+ import React from 'react';
14
+
15
+ type ResponseMessage = string | { message?: unknown };
16
+
17
+ interface APIClientApplication {
18
+ getName?: () => string | undefined;
19
+ context?: {
20
+ notification?: NotificationInstance;
21
+ };
22
+ }
23
+
24
+ interface NotificationError {
25
+ config?: AxiosRequestConfig & {
26
+ skipNotify?: boolean | ((error: unknown) => boolean);
27
+ };
28
+ }
29
+
30
+ const notificationCache = new Map<string, number>();
31
+
32
+ function getMessageText(item: ResponseMessage): string {
33
+ if (typeof item === 'string') {
34
+ return item;
35
+ }
36
+ return typeof item?.message === 'string' ? item.message : '';
37
+ }
38
+
39
+ function deduplicateMessages(messages: ResponseMessage[]): ResponseMessage[] {
40
+ if (notificationCache.size > 10) {
41
+ notificationCache.clear();
42
+ }
43
+ const now = Date.now();
44
+ return messages.filter((item) => {
45
+ const message = getMessageText(item);
46
+ if (!message) {
47
+ return false;
48
+ }
49
+ const lastTime = notificationCache.get(message);
50
+ if (lastTime && now - lastTime < 500) {
51
+ return false;
52
+ }
53
+ notificationCache.set(message, now);
54
+ return true;
55
+ });
56
+ }
57
+
58
+ function notify(type: 'success' | 'error', messages: ResponseMessage[], instance?: NotificationInstance) {
59
+ if (!instance || messages.length === 0) {
60
+ return;
61
+ }
62
+ const filteredMessages = deduplicateMessages(messages);
63
+ if (filteredMessages.length === 0) {
64
+ return;
65
+ }
66
+ instance[type]({
67
+ message: filteredMessages.map((item, index) => {
68
+ const message = getMessageText(item);
69
+ return React.createElement('div', { key: `${index}_${message}` }, message);
70
+ }),
71
+ });
72
+ }
11
73
 
12
74
  function offsetToTimeZone(offset: number) {
13
75
  const hours = Math.floor(Math.abs(offset));
@@ -23,13 +85,10 @@ function getCurrentTimezone() {
23
85
  }
24
86
 
25
87
  export class APIClient extends APIClientSDK {
26
- appName?: string;
88
+ app?: APIClientApplication;
27
89
 
28
- constructor(options?: APIClientOptions) {
29
- super(options);
30
- if (options && typeof options !== 'function') {
31
- this.appName = options.appName;
32
- }
90
+ get notification() {
91
+ return this.app?.context?.notification;
33
92
  }
34
93
 
35
94
  getHostname() {
@@ -64,5 +123,29 @@ export class APIClient extends APIClientSDK {
64
123
  return config;
65
124
  });
66
125
  super.interceptors();
126
+ this.useNotificationMiddleware();
127
+ }
128
+
129
+ handleNotificationError(error: unknown) {
130
+ const notificationError = error as NotificationError;
131
+ const skipNotify = notificationError.config?.skipNotify;
132
+ if (skipNotify && (skipNotify === true || (typeof skipNotify === 'function' && skipNotify(error)))) {
133
+ throw error;
134
+ }
135
+ const messages = this.toErrMessages(error);
136
+ if (Array.isArray(messages)) {
137
+ notify('error', messages, this.notification);
138
+ }
139
+ throw error;
140
+ }
141
+
142
+ useNotificationMiddleware() {
143
+ this.axios.interceptors.response.use((response) => {
144
+ const messages = response.data?.messages;
145
+ if (Array.isArray(messages)) {
146
+ notify('success', messages, this.notification);
147
+ }
148
+ return response;
149
+ }, this.handleNotificationError.bind(this));
67
150
  }
68
151
  }
@@ -45,10 +45,16 @@ export class Application extends BaseApplication<
45
45
  public hasLoadError = false;
46
46
 
47
47
  protected createApiClient(options: ApplicationOptions) {
48
- return new APIClient({
48
+ const apiClient = 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
  });
56
+ apiClient.app = this;
57
+ return apiClient;
52
58
  }
53
59
 
54
60
  protected configureRuntimeAdapters() {
@@ -30,10 +30,12 @@ import AntdAppProvider from './theme/AntdAppProvider';
30
30
  import { GlobalThemeProvider } from './theme';
31
31
  import { AIManager } from './ai';
32
32
  import { AppError, AppMaintaining, AppMaintainingDialog, AppNotFound, AppSpin, BlankComponent } from './components';
33
+ import { EntryActionManager } from './entry-actions';
33
34
  import { SystemSettingsSource } from './flow/system-settings';
34
35
  import { LayoutManager } from './layout-manager/LayoutManager';
35
36
  import type { PluginClass, PluginManager, PluginType } from './PluginManager';
36
37
  import { RouteRepository } from './RouteRepository';
38
+ import { stripModernClientPrefix } from './authRedirect';
37
39
  import type {
38
40
  ComponentTypeAndString,
39
41
  RenderableComponentType,
@@ -80,6 +82,10 @@ const trimTrailingSlashes = (value: string) => {
80
82
  return match ? value.slice(0, -match[0].length) : value;
81
83
  };
82
84
 
85
+ const ensureTrailingSlash = (value: string) => {
86
+ return `${trimTrailingSlashes(value)}/`;
87
+ };
88
+
83
89
  const isRenderableComponentType = (value: unknown): value is AnyComponent => isValidElementType(value);
84
90
 
85
91
  export type DevDynamicImport = (packageName: string) => Promise<{ default: PluginClass }>;
@@ -126,6 +132,7 @@ export abstract class BaseApplication<
126
132
  public pluginManager: TPluginManager;
127
133
  public pluginSettingsManager: TPluginSettingsManager;
128
134
  public layoutManager: LayoutManager<this>;
135
+ public entryActionManager = new EntryActionManager();
129
136
  public aiManager!: AIManager;
130
137
  public devDynamicImport?: DevDynamicImport;
131
138
  public requirejs!: RequireJS;
@@ -396,7 +403,7 @@ export abstract class BaseApplication<
396
403
  this.favicon = favicon || '';
397
404
  }
398
405
 
399
- const iconHref = this.favicon || '/favicon/favicon.ico';
406
+ const iconHref = this.favicon || this.getCdnUrl() + 'favicon/favicon.ico';
400
407
 
401
408
  if (faviconLinkElement) {
402
409
  faviconLinkElement.href = iconHref;
@@ -431,12 +438,12 @@ export abstract class BaseApplication<
431
438
  return getSubAppName(this.getPublicPath()) || null;
432
439
  }
433
440
 
441
+ getCdnUrl() {
442
+ return ensureTrailingSlash(window['__webpack_public_path__'] || stripModernClientPrefix(this.getPublicPath()));
443
+ }
444
+
434
445
  getPublicPath() {
435
- let publicPath = this.options.publicPath || '/';
436
- if (!publicPath.endsWith('/')) {
437
- publicPath += '/';
438
- }
439
- return publicPath;
446
+ return ensureTrailingSlash(this.options.publicPath || '/');
440
447
  }
441
448
 
442
449
  getApiUrl(pathname = '') {
@@ -268,6 +268,16 @@ export class RouteRepository {
268
268
  return this.findRoute(this.listAccessible(), schemaUid);
269
269
  }
270
270
 
271
+ /**
272
+ * 通过路由 id 反查对应路由节点。
273
+ *
274
+ * @param routeId 桌面路由主键
275
+ * @returns 匹配到的路由节点
276
+ */
277
+ getRouteById(routeId: string | number): NocoBaseDesktopRoute | undefined {
278
+ return this.findRouteById(this.listAccessible(), routeId);
279
+ }
280
+
271
281
  protected getAPIClient(): APIClient {
272
282
  if (!this.ctx?.api) {
273
283
  throw new Error('[NocoBase] RouteRepository requires context.api.');
@@ -346,4 +356,19 @@ export class RouteRepository {
346
356
  }
347
357
  return undefined;
348
358
  }
359
+
360
+ private findRouteById(routes: NocoBaseDesktopRoute[], routeId: string | number): NocoBaseDesktopRoute | undefined {
361
+ for (const route of routes) {
362
+ if (route.id != null && String(route.id) === String(routeId)) {
363
+ return route;
364
+ }
365
+ if (route.children) {
366
+ const found = this.findRouteById(route.children, routeId);
367
+ if (found) {
368
+ return found;
369
+ }
370
+ }
371
+ }
372
+ return undefined;
373
+ }
349
374
  }
@@ -26,6 +26,7 @@ import type { BaseApplication } from './BaseApplication';
26
26
  import { BlankComponent, RouterContextCleaner } from './components';
27
27
  import { RouterBridge } from './components/RouterBridge';
28
28
  import { Router } from '@remix-run/router';
29
+ import { getV2EffectiveBasePath } from './authRedirect';
29
30
 
30
31
  export interface BrowserRouterOptions extends Omit<BrowserRouterProps, 'children'> {
31
32
  type?: 'browser';
@@ -55,6 +56,87 @@ export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
55
56
  export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
56
57
  export type RouterComponentType = React.FC<{ BaseLayout?: ComponentType }>;
57
58
 
59
+ const DEFAULT_ADMIN_ROUTE_PATH = '/admin';
60
+
61
+ type AdminRouteNavigationTarget = {
62
+ currentPathname?: string;
63
+ targetPathname: unknown;
64
+ basePath?: string;
65
+ adminRoutePath?: string;
66
+ replace?: boolean;
67
+ };
68
+
69
+ function trimPathSearchAndHash(pathname: string) {
70
+ return pathname.split(/[?#]/)[0];
71
+ }
72
+
73
+ function splitPathSearchAndHash(pathname: string) {
74
+ const match = pathname.match(/^([^?#]*)(.*)$/);
75
+ return {
76
+ pathname: match?.[1] || '',
77
+ suffix: match?.[2] || '',
78
+ };
79
+ }
80
+
81
+ function normalizeRootPath(pathname?: string) {
82
+ const trimmed = trimPathSearchAndHash(pathname || '').trim();
83
+ if (!trimmed || trimmed === '/') {
84
+ return '/';
85
+ }
86
+ return `/${trimmed.replace(/^\/+|\/+$/g, '')}`;
87
+ }
88
+
89
+ function isRootRelativePath(pathname: string) {
90
+ return pathname.startsWith('/') && !pathname.startsWith('//') && !pathname.startsWith('/\\');
91
+ }
92
+
93
+ function removeBasePath(pathname: string, basePath?: string) {
94
+ const normalizedPathname = normalizeRootPath(pathname);
95
+ const normalizedBasePath = normalizeRootPath(basePath);
96
+
97
+ if (normalizedBasePath === '/') {
98
+ return normalizedPathname;
99
+ }
100
+ if (normalizedPathname === normalizedBasePath) {
101
+ return '/';
102
+ }
103
+ if (normalizedPathname.startsWith(`${normalizedBasePath}/`)) {
104
+ return normalizeRootPath(normalizedPathname.slice(normalizedBasePath.length));
105
+ }
106
+ return normalizedPathname;
107
+ }
108
+
109
+ function removeAppSegment(pathname: string) {
110
+ return normalizeRootPath(pathname).replace(/^\/(?:apps|_app)\/[^/]+(?=\/|$)/, '') || '/';
111
+ }
112
+
113
+ function normalizePortalRoutePath(pathname: string, basePath?: string) {
114
+ return removeAppSegment(removeBasePath(pathname, basePath));
115
+ }
116
+
117
+ function isSameOrChildPath(pathname: string, basePath: string) {
118
+ const normalizedPathname = normalizeRootPath(pathname);
119
+ const normalizedBasePath = normalizeRootPath(basePath);
120
+ return normalizedPathname === normalizedBasePath || normalizedPathname.startsWith(`${normalizedBasePath}/`);
121
+ }
122
+
123
+ export function shouldOpenAdminRouteInNewWindow(options: AdminRouteNavigationTarget) {
124
+ if (options.replace || typeof options.targetPathname !== 'string') {
125
+ return false;
126
+ }
127
+
128
+ const targetPathname = options.targetPathname.trim();
129
+ if (!isRootRelativePath(targetPathname)) {
130
+ return false;
131
+ }
132
+
133
+ const adminRoutePath = normalizeRootPath(options.adminRoutePath || DEFAULT_ADMIN_ROUTE_PATH);
134
+ const currentRoutePath = normalizePortalRoutePath(options.currentPathname || '/', options.basePath);
135
+ const targetRoutePath = normalizePortalRoutePath(targetPathname, options.basePath);
136
+
137
+ return !isSameOrChildPath(currentRoutePath, adminRoutePath) && isSameOrChildPath(targetRoutePath, adminRoutePath);
138
+ }
139
+
58
140
  function removeBasename(pathname: string, basename?: string) {
59
141
  if (!basename || basename === '/') {
60
142
  return pathname;
@@ -72,6 +154,7 @@ function removeBasename(pathname: string, basename?: string) {
72
154
  export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<any>> {
73
155
  protected routes: Record<string, RouteType> = {};
74
156
  protected options: RouterOptions;
157
+ private routerNavigate?: Router['navigate'];
75
158
  public app: TApp;
76
159
  public router!: Router;
77
160
  get basename() {
@@ -81,7 +164,7 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
81
164
  return this.router.state;
82
165
  }
83
166
  get navigate() {
84
- return this.router.navigate;
167
+ return this.navigateWithPortalPolicy;
85
168
  }
86
169
 
87
170
  constructor(options: RouterOptions = {}, app: TApp) {
@@ -90,6 +173,62 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
90
173
  this.routes = options.routes || {};
91
174
  }
92
175
 
176
+ private navigateWithPortalPolicy: Router['navigate'] = ((to, opts) => {
177
+ if (this.shouldOpenAdminRouteInNewWindow(to, opts)) {
178
+ window.open(this.getAdminRouteNavigationHref(to as string), '_blank', 'noopener,noreferrer');
179
+ return Promise.resolve();
180
+ }
181
+
182
+ const navigate = this.routerNavigate || this.router.navigate.bind(this.router);
183
+ return navigate(to, opts);
184
+ }) as Router['navigate'];
185
+
186
+ private shouldOpenAdminRouteInNewWindow(
187
+ to: Parameters<Router['navigate']>[0],
188
+ opts?: Parameters<Router['navigate']>[1],
189
+ ) {
190
+ if (this.options.type && this.options.type !== 'browser') {
191
+ return false;
192
+ }
193
+
194
+ return shouldOpenAdminRouteInNewWindow({
195
+ currentPathname:
196
+ typeof window !== 'undefined' ? window.location.pathname : this.router?.state?.location?.pathname,
197
+ targetPathname: to,
198
+ basePath: this.getRuntimeBasePath(),
199
+ adminRoutePath: this.getAdminRoutePath(),
200
+ replace: opts?.replace,
201
+ });
202
+ }
203
+
204
+ private getRuntimeBasePath() {
205
+ return getV2EffectiveBasePath(this.app);
206
+ }
207
+
208
+ private getAdminRoutePath() {
209
+ try {
210
+ return this.app.layoutManager?.getLayout?.('admin')?.routePath || DEFAULT_ADMIN_ROUTE_PATH;
211
+ } catch {
212
+ return DEFAULT_ADMIN_ROUTE_PATH;
213
+ }
214
+ }
215
+
216
+ private getAdminRouteNavigationHref(pathname: string) {
217
+ const { pathname: rawPathname, suffix } = splitPathSearchAndHash(pathname.trim());
218
+ const normalizedPathname = normalizeRootPath(rawPathname);
219
+ const runtimeBasePath = this.getRuntimeBasePath();
220
+ const normalizedBasePath = normalizeRootPath(runtimeBasePath);
221
+
222
+ if (
223
+ normalizedBasePath !== '/' &&
224
+ (normalizedPathname === normalizedBasePath || normalizedPathname.startsWith(`${normalizedBasePath}/`))
225
+ ) {
226
+ return `${normalizedPathname}${suffix}`;
227
+ }
228
+
229
+ return `${this.app.getHref(normalizePortalRoutePath(normalizedPathname, runtimeBasePath))}${suffix}`;
230
+ }
231
+
93
232
  protected resolveLoadedComponent(moduleOrComponent: ComponentLoaderResult): ComponentTypeAndString | undefined {
94
233
  if (!moduleOrComponent) {
95
234
  return undefined;
@@ -257,6 +396,8 @@ export class RouterManager<TApp extends BaseApplication<any> = BaseApplication<a
257
396
  ],
258
397
  opts,
259
398
  );
399
+ this.routerNavigate = this.router.navigate.bind(this.router) as Router['navigate'];
400
+ this.router.navigate = this.navigateWithPortalPolicy;
260
401
 
261
402
  const RenderRouter: RouterComponentType = ({ BaseLayout = BlankComponent }) => {
262
403
  return (
@@ -0,0 +1,58 @@
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 { APIClient } from '@nocobase/client-v2';
11
+ import type { NotificationInstance } from 'antd/es/notification/interface';
12
+ import MockAdapter from 'axios-mock-adapter';
13
+ import React from 'react';
14
+ import { renderToStaticMarkup } from 'react-dom/server';
15
+
16
+ function createAPIClient() {
17
+ const notification = {
18
+ error: vi.fn(),
19
+ success: vi.fn(),
20
+ } as unknown as NotificationInstance;
21
+ const apiClient = new APIClient();
22
+ apiClient.app = { context: { notification } };
23
+ const apiMock = new MockAdapter(apiClient.axios);
24
+ return { apiClient, apiMock, notification };
25
+ }
26
+
27
+ describe('APIClient response notifications', () => {
28
+ it('shows error messages returned by failed requests', async () => {
29
+ const { apiClient, apiMock, notification } = createAPIClient();
30
+ apiMock.onPost('/posts:create').reply(400, { errors: [{ message: 'Request rejected by workflow' }] });
31
+
32
+ await expect(apiClient.request({ url: '/posts:create', method: 'post' })).rejects.toBeDefined();
33
+
34
+ expect(notification.error).toHaveBeenCalledOnce();
35
+ const [{ message }] = vi.mocked(notification.error).mock.calls[0];
36
+ expect(renderToStaticMarkup(<>{message}</>)).toContain('Request rejected by workflow');
37
+ });
38
+
39
+ it('does not show errors when skipNotify is enabled', async () => {
40
+ const { apiClient, apiMock, notification } = createAPIClient();
41
+ apiMock.onGet('/health').reply(500, { errors: [{ message: 'Health check failed' }] });
42
+
43
+ await expect(apiClient.request({ url: '/health', skipNotify: true })).rejects.toBeDefined();
44
+
45
+ expect(notification.error).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it('shows messages returned by successful requests', async () => {
49
+ const { apiClient, apiMock, notification } = createAPIClient();
50
+ apiMock.onPost('/posts:create').reply(200, { messages: [{ message: 'Created successfully' }] });
51
+
52
+ await apiClient.request({ url: '/posts:create', method: 'post' });
53
+
54
+ expect(notification.success).toHaveBeenCalledOnce();
55
+ const [{ message }] = vi.mocked(notification.success).mock.calls[0];
56
+ expect(renderToStaticMarkup(<>{message}</>)).toContain('Created successfully');
57
+ });
58
+ });
@@ -140,6 +140,29 @@ describe('RouteRepository', () => {
140
140
  expect(repository.getRouteBySchemaUid('custom-page')).toBeUndefined();
141
141
  });
142
142
 
143
+ it('should find nested routes by id', () => {
144
+ const { repository } = createRouteRepository();
145
+
146
+ repository.setRoutes([
147
+ {
148
+ id: 1,
149
+ schemaUid: 'group-1',
150
+ children: [
151
+ {
152
+ id: 11,
153
+ schemaUid: 'page-1',
154
+ },
155
+ ],
156
+ },
157
+ ] as NocoBaseDesktopRoute[]);
158
+
159
+ expect(repository.getRouteById('11')).toMatchObject({
160
+ id: 11,
161
+ schemaUid: 'page-1',
162
+ });
163
+ expect(repository.getRouteById('missing')).toBeUndefined();
164
+ });
165
+
143
166
  it('should notify only subscribers for the changed layout cache', () => {
144
167
  const { repository } = createRouteRepository();
145
168
  const adminEvents: string[][] = [];
@@ -0,0 +1,125 @@
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 type { BaseApplication } from '../BaseApplication';
11
+ import { RouterManager, shouldOpenAdminRouteInNewWindow } from '../RouterManager';
12
+
13
+ describe('RouterManager', () => {
14
+ describe('shouldOpenAdminRouteInNewWindow', () => {
15
+ it('opens admin route in a new window from a non-admin portal', () => {
16
+ expect(
17
+ shouldOpenAdminRouteInNewWindow({
18
+ currentPathname: '/v/test',
19
+ targetPathname: '/admin/settings',
20
+ basePath: '/v',
21
+ adminRoutePath: '/admin',
22
+ }),
23
+ ).toBe(true);
24
+ });
25
+
26
+ it('supports target pathname with the v2 base path', () => {
27
+ expect(
28
+ shouldOpenAdminRouteInNewWindow({
29
+ currentPathname: '/nocobase/v/test',
30
+ targetPathname: '/nocobase/v/admin/settings',
31
+ basePath: '/nocobase/v',
32
+ adminRoutePath: '/admin',
33
+ }),
34
+ ).toBe(true);
35
+ });
36
+
37
+ it('supports custom portal routes in sub-apps', () => {
38
+ expect(
39
+ shouldOpenAdminRouteInNewWindow({
40
+ currentPathname: '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu',
41
+ targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
42
+ basePath: '/v',
43
+ adminRoutePath: '/admin',
44
+ }),
45
+ ).toBe(true);
46
+ });
47
+
48
+ it('keeps admin route navigation in the current window when already in sub-app admin', () => {
49
+ expect(
50
+ shouldOpenAdminRouteInNewWindow({
51
+ currentPathname: '/v/apps/a_9xlild35jir/admin',
52
+ targetPathname: '/v/apps/a_9xlild35jir/admin/settings',
53
+ basePath: '/v',
54
+ adminRoutePath: '/admin',
55
+ }),
56
+ ).toBe(false);
57
+ });
58
+
59
+ it('keeps admin route navigation in the current window when already in admin', () => {
60
+ expect(
61
+ shouldOpenAdminRouteInNewWindow({
62
+ currentPathname: '/v/admin',
63
+ targetPathname: '/admin/settings',
64
+ basePath: '/v',
65
+ adminRoutePath: '/admin',
66
+ }),
67
+ ).toBe(false);
68
+ });
69
+
70
+ it('keeps non-admin portal navigation in the current window', () => {
71
+ expect(
72
+ shouldOpenAdminRouteInNewWindow({
73
+ currentPathname: '/v/test',
74
+ targetPathname: '/test/page',
75
+ basePath: '/v',
76
+ adminRoutePath: '/admin',
77
+ }),
78
+ ).toBe(false);
79
+ });
80
+
81
+ it('does not intercept replace navigation or relative paths', () => {
82
+ expect(
83
+ shouldOpenAdminRouteInNewWindow({
84
+ currentPathname: '/v/test',
85
+ targetPathname: '/admin/settings',
86
+ basePath: '/v',
87
+ adminRoutePath: '/admin',
88
+ replace: true,
89
+ }),
90
+ ).toBe(false);
91
+ expect(
92
+ shouldOpenAdminRouteInNewWindow({
93
+ currentPathname: '/v/test',
94
+ targetPathname: 'admin/settings',
95
+ basePath: '/v',
96
+ adminRoutePath: '/admin',
97
+ }),
98
+ ).toBe(false);
99
+ });
100
+ });
101
+
102
+ it('opens admin route in a new window through the underlying router navigate from a sub-app portal', async () => {
103
+ const app = {
104
+ name: 'a_9xlild35jir',
105
+ getPublicPath: () => '/v/',
106
+ getHref: (pathname: string) => `/v/apps/a_9xlild35jir/${pathname.replace(/^\/+/, '')}`,
107
+ layoutManager: {
108
+ getLayout: () => ({ routePath: '/admin' }),
109
+ },
110
+ renderComponent: () => null,
111
+ } as unknown as BaseApplication<unknown>;
112
+ const manager = new RouterManager({ type: 'browser' }, app);
113
+ const open = vi.spyOn(window, 'open').mockImplementation(() => null);
114
+
115
+ window.history.pushState({}, '', '/v/apps/a_9xlild35jir/crm-amd/ekeisumx1zu');
116
+ manager.getRouterComponent();
117
+ await manager.router.navigate('/admin/settings/version-control/list');
118
+
119
+ expect(open).toHaveBeenCalledWith(
120
+ '/v/apps/a_9xlild35jir/admin/settings/version-control/list',
121
+ '_blank',
122
+ 'noopener,noreferrer',
123
+ );
124
+ });
125
+ });