@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
@@ -9,11 +9,12 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
- import { MemoryRouter, Route, Routes } from 'react-router-dom';
13
- import { render, screen, waitFor } from '@testing-library/react';
12
+ import { MemoryRouter, Route, Router, Routes, useNavigate } from 'react-router-dom';
13
+ import { act, render, screen, waitFor } from '@testing-library/react';
14
14
  import { FlowContextProvider, FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
15
15
  import FlowRoute from '../components/FlowRoute';
16
16
  import { RouteRepository } from '../../RouteRepository';
17
+ import { NocoBaseDesktopRouteType } from '../../flow-compat';
17
18
 
18
19
  type MockAdminLayoutModel = FlowModel & {
19
20
  registerRoutePage: ReturnType<typeof vi.fn>;
@@ -21,6 +22,8 @@ type MockAdminLayoutModel = FlowModel & {
21
22
  unregisterRoutePage: ReturnType<typeof vi.fn>;
22
23
  };
23
24
 
25
+ type RouterNavigator = React.ComponentProps<typeof Router>['navigator'];
26
+
24
27
  const { hookState } = vi.hoisted(() => {
25
28
  return {
26
29
  hookState: {
@@ -113,6 +116,7 @@ describe('FlowRoute', () => {
113
116
  }),
114
117
  );
115
118
  });
119
+ expect(engine.context.deviceType).toBe('computer');
116
120
 
117
121
  rerender(
118
122
  <FlowEngineProvider engine={engine}>
@@ -578,14 +582,14 @@ describe('FlowRoute', () => {
578
582
  expect(getRouteBySchemaUid).toHaveBeenCalledTimes(1);
579
583
  });
580
584
 
581
- it('should show 404 when current route is a legacy page in v2 runtime', async () => {
585
+ it('should explain how to open a legacy page outside the v2 runtime', async () => {
582
586
  const originalLocation = window.location;
583
587
  const replace = vi.fn();
584
588
  Object.defineProperty(window, 'location', {
585
589
  configurable: true,
586
590
  value: {
587
591
  ...originalLocation,
588
- pathname: '/v2/admin/test-page/tab/tab-1',
592
+ pathname: '/nocobase/v2/apps/jhb20/admin/test-page/tab/tab-1',
589
593
  search: '?from=direct',
590
594
  hash: '#dialog',
591
595
  replace,
@@ -604,9 +608,9 @@ describe('FlowRoute', () => {
604
608
  });
605
609
  engine.context.defineProperty('app', {
606
610
  value: {
607
- getPublicPath: () => '/v2/',
611
+ getPublicPath: () => '/nocobase/v2/',
608
612
  router: {
609
- getBasename: () => '/v2',
613
+ getBasename: () => '/nocobase/v2',
610
614
  },
611
615
  },
612
616
  });
@@ -622,15 +626,25 @@ describe('FlowRoute', () => {
622
626
 
623
627
  render(
624
628
  <FlowEngineProvider engine={engine}>
625
- <MemoryRouter initialEntries={['/flow/test-page']}>
629
+ <MemoryRouter initialEntries={['/nocobase/v2/apps/jhb20/admin/test-page/tab/tab-1?from=direct#dialog']}>
626
630
  <Routes>
627
- <Route path="/flow/:name" element={<FlowRoute />} />
631
+ <Route path="/nocobase/v2/apps/jhb20/admin/:name/*" element={<FlowRoute />} />
628
632
  </Routes>
629
633
  </MemoryRouter>
630
634
  </FlowEngineProvider>,
631
635
  );
632
636
 
633
- expect(await screen.findByText('404')).toBeInTheDocument();
637
+ expect(await screen.findByText('This page is not supported in the /v2/ branch')).toBeInTheDocument();
638
+ expect(
639
+ screen.getByText(
640
+ 'The /v2/ branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
641
+ ),
642
+ ).toBeInTheDocument();
643
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
644
+ expect(screen.getByRole('link', { name: 'Open from the original entry' })).toHaveAttribute(
645
+ 'href',
646
+ '/nocobase/apps/jhb20/admin/test-page/tab/tab-1?from=direct#dialog',
647
+ );
634
648
  expect(replace).not.toHaveBeenCalled();
635
649
  expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
636
650
  expect(adminLayoutModel.updateRoutePage).not.toHaveBeenCalled();
@@ -703,6 +717,113 @@ describe('FlowRoute', () => {
703
717
  }
704
718
  });
705
719
 
720
+ it('should render 404 for missing FlowModel in notFound mode without routeRepository', async () => {
721
+ const engine = new FlowEngine();
722
+ engine.context.defineProperty('app', {
723
+ value: {
724
+ getPublicPath: () => '/v2/',
725
+ router: {
726
+ getBasename: () => '/v2',
727
+ },
728
+ },
729
+ });
730
+
731
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
732
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
733
+ {
734
+ registerRoutePage: vi.fn(),
735
+ updateRoutePage: vi.fn(),
736
+ unregisterRoutePage: vi.fn(),
737
+ },
738
+ );
739
+
740
+ render(
741
+ <FlowEngineProvider engine={engine}>
742
+ <MemoryRouter initialEntries={['/embed/missing-page']}>
743
+ <Routes>
744
+ <Route path="/embed/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
745
+ </Routes>
746
+ </MemoryRouter>
747
+ </FlowEngineProvider>,
748
+ );
749
+
750
+ expect(await screen.findByText('404')).toBeInTheDocument();
751
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
752
+ });
753
+
754
+ it('should bridge by default when routeRepository does not exist', async () => {
755
+ const engine = new FlowEngine();
756
+ engine.context.defineProperty('app', {
757
+ value: {
758
+ getPublicPath: () => '/v2/',
759
+ router: {
760
+ getBasename: () => '/v2',
761
+ },
762
+ },
763
+ });
764
+
765
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
766
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
767
+ {
768
+ registerRoutePage: vi.fn(),
769
+ updateRoutePage: vi.fn(),
770
+ unregisterRoutePage: vi.fn(),
771
+ },
772
+ );
773
+
774
+ render(
775
+ <FlowEngineProvider engine={engine}>
776
+ <MemoryRouter initialEntries={['/flow/missing-page']}>
777
+ <Routes>
778
+ <Route path="/flow/:name" element={<FlowRoute />} />
779
+ </Routes>
780
+ </MemoryRouter>
781
+ </FlowEngineProvider>,
782
+ );
783
+
784
+ await waitFor(() => {
785
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('missing-page', expect.any(Object));
786
+ });
787
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
788
+ });
789
+
790
+ it('should bridge existing FlowModel in notFound mode without routeRepository', async () => {
791
+ const engine = new FlowEngine();
792
+ engine.createModel({ uid: 'test-page', use: 'FlowModel' });
793
+ engine.context.defineProperty('app', {
794
+ value: {
795
+ getPublicPath: () => '/v2/',
796
+ router: {
797
+ getBasename: () => '/v2',
798
+ },
799
+ },
800
+ });
801
+
802
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
803
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
804
+ {
805
+ registerRoutePage: vi.fn(),
806
+ updateRoutePage: vi.fn(),
807
+ unregisterRoutePage: vi.fn(),
808
+ },
809
+ );
810
+
811
+ render(
812
+ <FlowEngineProvider engine={engine}>
813
+ <MemoryRouter initialEntries={['/embed/test-page']}>
814
+ <Routes>
815
+ <Route path="/embed/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
816
+ </Routes>
817
+ </MemoryRouter>
818
+ </FlowEngineProvider>,
819
+ );
820
+
821
+ await waitFor(() => {
822
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('test-page', expect.any(Object));
823
+ });
824
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
825
+ });
826
+
706
827
  it('should bridge existing FlowModel when behavior is notFound and routeRepository has no route', async () => {
707
828
  const engine = new FlowEngine();
708
829
  engine.setModelRepository({
@@ -893,6 +1014,548 @@ describe('FlowRoute', () => {
893
1014
  expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
894
1015
  });
895
1016
 
1017
+ it('should render blank content when current admin route matches an accessible empty group id', async () => {
1018
+ const engine = new FlowEngine();
1019
+ const listAccessible = vi.fn(() => [
1020
+ {
1021
+ id: 371750686228480,
1022
+ schemaUid: 'group-schema',
1023
+ title: '789',
1024
+ type: NocoBaseDesktopRouteType.group,
1025
+ },
1026
+ ]);
1027
+ engine.context.defineProperty('routeRepository', {
1028
+ value: {
1029
+ refreshAccessible: hookState.refresh,
1030
+ isAccessibleLoaded: () => true,
1031
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1032
+ getRouteBySchemaUid: vi.fn(() => undefined),
1033
+ listAccessible,
1034
+ },
1035
+ });
1036
+ engine.context.defineProperty('app', {
1037
+ value: {
1038
+ getPublicPath: () => '/v2/',
1039
+ router: {
1040
+ getBasename: () => '/v2',
1041
+ },
1042
+ },
1043
+ });
1044
+
1045
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1046
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1047
+ {
1048
+ registerRoutePage: vi.fn(),
1049
+ updateRoutePage: vi.fn(),
1050
+ unregisterRoutePage: vi.fn(),
1051
+ },
1052
+ );
1053
+
1054
+ const { container } = render(
1055
+ <FlowEngineProvider engine={engine}>
1056
+ <MemoryRouter initialEntries={['/admin/371750686228480']}>
1057
+ <Routes>
1058
+ <Route path="/admin/:name" element={<FlowRoute />} />
1059
+ </Routes>
1060
+ </MemoryRouter>
1061
+ </FlowEngineProvider>,
1062
+ );
1063
+
1064
+ await waitFor(() => {
1065
+ expect(listAccessible).toHaveBeenCalled();
1066
+ });
1067
+ expect(container).toBeEmptyDOMElement();
1068
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
1069
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1070
+ });
1071
+
1072
+ it('should navigate a current admin group route to its first accessible flow page', async () => {
1073
+ const engine = new FlowEngine();
1074
+ const childRoute = {
1075
+ schemaUid: 'child-flow-page',
1076
+ title: 'Child flow page',
1077
+ type: NocoBaseDesktopRouteType.flowPage,
1078
+ };
1079
+ const groupRoute = {
1080
+ id: 371750686228480,
1081
+ schemaUid: 'group-schema',
1082
+ title: 'Group with child',
1083
+ type: NocoBaseDesktopRouteType.group,
1084
+ children: [childRoute],
1085
+ };
1086
+ const getRouteBySchemaUid = vi.fn((schemaUid: string) =>
1087
+ schemaUid === 'child-flow-page' ? childRoute : undefined,
1088
+ );
1089
+ const listAccessible = vi.fn(() => [groupRoute]);
1090
+ engine.context.defineProperty('routeRepository', {
1091
+ value: {
1092
+ refreshAccessible: hookState.refresh,
1093
+ isAccessibleLoaded: () => true,
1094
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1095
+ getRouteBySchemaUid,
1096
+ listAccessible,
1097
+ },
1098
+ });
1099
+ engine.context.defineProperty('app', {
1100
+ value: {
1101
+ getPublicPath: () => '/v2/',
1102
+ router: {
1103
+ getBasename: () => '/v2',
1104
+ },
1105
+ },
1106
+ });
1107
+
1108
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1109
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1110
+ {
1111
+ registerRoutePage: vi.fn(),
1112
+ updateRoutePage: vi.fn(),
1113
+ unregisterRoutePage: vi.fn(),
1114
+ },
1115
+ );
1116
+
1117
+ render(
1118
+ <FlowEngineProvider engine={engine}>
1119
+ <MemoryRouter initialEntries={['/admin/371750686228480']}>
1120
+ <Routes>
1121
+ <Route path="/admin/:name" element={<FlowRoute />} />
1122
+ </Routes>
1123
+ </MemoryRouter>
1124
+ </FlowEngineProvider>,
1125
+ );
1126
+
1127
+ await waitFor(() => {
1128
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page', expect.any(Object));
1129
+ });
1130
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
1131
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
1132
+ });
1133
+
1134
+ it('should keep rendering 404 when a group id is opened with a nested admin route path', async () => {
1135
+ const engine = new FlowEngine();
1136
+ const childRoute = {
1137
+ schemaUid: 'child-flow-page',
1138
+ title: 'Child flow page',
1139
+ type: NocoBaseDesktopRouteType.flowPage,
1140
+ };
1141
+ const groupRoute = {
1142
+ id: 371750686228480,
1143
+ schemaUid: 'group-schema',
1144
+ title: 'Group with child',
1145
+ type: NocoBaseDesktopRouteType.group,
1146
+ children: [childRoute],
1147
+ };
1148
+ engine.context.defineProperty('routeRepository', {
1149
+ value: {
1150
+ refreshAccessible: hookState.refresh,
1151
+ isAccessibleLoaded: () => true,
1152
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1153
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
1154
+ listAccessible: vi.fn(() => [groupRoute]),
1155
+ },
1156
+ });
1157
+ engine.context.defineProperty('app', {
1158
+ value: {
1159
+ getPublicPath: () => '/v2/',
1160
+ router: {
1161
+ getBasename: () => '/v2',
1162
+ },
1163
+ },
1164
+ });
1165
+
1166
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1167
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1168
+ {
1169
+ registerRoutePage: vi.fn(),
1170
+ updateRoutePage: vi.fn(),
1171
+ unregisterRoutePage: vi.fn(),
1172
+ },
1173
+ );
1174
+
1175
+ render(
1176
+ <FlowEngineProvider engine={engine}>
1177
+ <MemoryRouter initialEntries={['/admin/371750686228480/view/not-exists']}>
1178
+ <Routes>
1179
+ <Route path="/admin/:name/view/*" element={<FlowRoute />} />
1180
+ </Routes>
1181
+ </MemoryRouter>
1182
+ </FlowEngineProvider>,
1183
+ );
1184
+
1185
+ expect(await screen.findByText('404')).toBeInTheDocument();
1186
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('child-flow-page', expect.any(Object));
1187
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
1188
+ });
1189
+
1190
+ it('should keep rendering 404 when a group id is opened with a tab admin route path', async () => {
1191
+ const engine = new FlowEngine();
1192
+ const childRoute = {
1193
+ schemaUid: 'child-flow-page',
1194
+ title: 'Child flow page',
1195
+ type: NocoBaseDesktopRouteType.flowPage,
1196
+ };
1197
+ const groupRoute = {
1198
+ id: 371750686228480,
1199
+ schemaUid: 'group-schema',
1200
+ title: 'Group with child',
1201
+ type: NocoBaseDesktopRouteType.group,
1202
+ children: [childRoute],
1203
+ };
1204
+ engine.context.defineProperty('routeRepository', {
1205
+ value: {
1206
+ refreshAccessible: hookState.refresh,
1207
+ isAccessibleLoaded: () => true,
1208
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1209
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
1210
+ listAccessible: vi.fn(() => [groupRoute]),
1211
+ },
1212
+ });
1213
+ engine.context.defineProperty('app', {
1214
+ value: {
1215
+ getPublicPath: () => '/v2/',
1216
+ router: {
1217
+ getBasename: () => '/v2',
1218
+ },
1219
+ },
1220
+ });
1221
+
1222
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1223
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1224
+ {
1225
+ registerRoutePage: vi.fn(),
1226
+ updateRoutePage: vi.fn(),
1227
+ unregisterRoutePage: vi.fn(),
1228
+ },
1229
+ );
1230
+
1231
+ render(
1232
+ <FlowEngineProvider engine={engine}>
1233
+ <MemoryRouter initialEntries={['/admin/371750686228480/tab/not-exists']}>
1234
+ <Routes>
1235
+ <Route path="/admin/:name/tab/:tabUid" element={<FlowRoute />} />
1236
+ </Routes>
1237
+ </MemoryRouter>
1238
+ </FlowEngineProvider>,
1239
+ );
1240
+
1241
+ expect(await screen.findByText('404')).toBeInTheDocument();
1242
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('child-flow-page', expect.any(Object));
1243
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
1244
+ });
1245
+
1246
+ it('should keep blank content while a group child navigation is already pending for the current path', async () => {
1247
+ const engine = new FlowEngine();
1248
+ const childRoute = {
1249
+ schemaUid: 'child-flow-page',
1250
+ title: 'Child flow page',
1251
+ type: NocoBaseDesktopRouteType.flowPage,
1252
+ };
1253
+ const groupRoute = {
1254
+ id: 371750686228480,
1255
+ schemaUid: 'group-schema',
1256
+ title: 'Group with child',
1257
+ type: NocoBaseDesktopRouteType.group,
1258
+ children: [childRoute],
1259
+ };
1260
+ engine.context.defineProperty('routeRepository', {
1261
+ value: {
1262
+ refreshAccessible: hookState.refresh,
1263
+ isAccessibleLoaded: () => true,
1264
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1265
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
1266
+ listAccessible: vi.fn(() => [groupRoute]),
1267
+ },
1268
+ });
1269
+ engine.context.defineProperty('app', {
1270
+ value: {
1271
+ getPublicPath: () => '/v2/',
1272
+ router: {
1273
+ getBasename: () => '/v2',
1274
+ },
1275
+ },
1276
+ });
1277
+
1278
+ const replace = vi.fn();
1279
+ const navigator: RouterNavigator = {
1280
+ createHref: (to: { pathname?: string; search?: string; hash?: string } | string) =>
1281
+ typeof to === 'string' ? to : `${to.pathname || ''}${to.search || ''}${to.hash || ''}`,
1282
+ go: vi.fn(),
1283
+ push: vi.fn(),
1284
+ replace,
1285
+ };
1286
+
1287
+ const RerenderAfterReplace = () => {
1288
+ const [rerendered, setRerendered] = React.useState(false);
1289
+ React.useEffect(() => {
1290
+ if (!rerendered && replace.mock.calls.length > 0) {
1291
+ setRerendered(true);
1292
+ }
1293
+ }, [rerendered]);
1294
+
1295
+ return <FlowRoute legacyPageBehavior={rerendered ? 'bridge' : undefined} />;
1296
+ };
1297
+
1298
+ render(
1299
+ <FlowEngineProvider engine={engine}>
1300
+ <Router location="/admin/371750686228480" navigator={navigator}>
1301
+ <Routes>
1302
+ <Route path="/admin/:name" element={<RerenderAfterReplace />} />
1303
+ </Routes>
1304
+ </Router>
1305
+ </FlowEngineProvider>,
1306
+ );
1307
+
1308
+ await waitFor(() => {
1309
+ expect(replace).toHaveBeenCalled();
1310
+ });
1311
+ await act(async () => {
1312
+ await new Promise((resolve) => setTimeout(resolve, 0));
1313
+ });
1314
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
1315
+ });
1316
+
1317
+ it('should reset group navigation guard when page uid changes in the same route instance', async () => {
1318
+ const engine = new FlowEngine();
1319
+ const childRoute1 = {
1320
+ schemaUid: 'child-flow-page-1',
1321
+ title: 'Child flow page 1',
1322
+ type: NocoBaseDesktopRouteType.flowPage,
1323
+ };
1324
+ const childRoute2 = {
1325
+ schemaUid: 'child-flow-page-2',
1326
+ title: 'Child flow page 2',
1327
+ type: NocoBaseDesktopRouteType.flowPage,
1328
+ };
1329
+ const groupRoute1 = {
1330
+ id: 371750686228480,
1331
+ schemaUid: 'group-schema-1',
1332
+ title: 'Group with child 1',
1333
+ type: NocoBaseDesktopRouteType.group,
1334
+ children: [childRoute1],
1335
+ };
1336
+ const groupRoute2 = {
1337
+ id: 371750755434496,
1338
+ schemaUid: 'group-schema-2',
1339
+ title: 'Group with child 2',
1340
+ type: NocoBaseDesktopRouteType.group,
1341
+ children: [childRoute2],
1342
+ };
1343
+ const routeMap: Record<string, typeof childRoute1 | typeof childRoute2> = {
1344
+ 'child-flow-page-1': childRoute1,
1345
+ 'child-flow-page-2': childRoute2,
1346
+ };
1347
+ const getRouteBySchemaUid = vi.fn((schemaUid: string) => routeMap[schemaUid]);
1348
+ const listAccessible = vi.fn(() => [groupRoute1, groupRoute2]);
1349
+ engine.context.defineProperty('routeRepository', {
1350
+ value: {
1351
+ refreshAccessible: hookState.refresh,
1352
+ isAccessibleLoaded: () => true,
1353
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1354
+ getRouteBySchemaUid,
1355
+ listAccessible,
1356
+ },
1357
+ });
1358
+ engine.context.defineProperty('app', {
1359
+ value: {
1360
+ getPublicPath: () => '/v2/',
1361
+ router: {
1362
+ getBasename: () => '/v2',
1363
+ },
1364
+ },
1365
+ });
1366
+
1367
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1368
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1369
+ {
1370
+ registerRoutePage: vi.fn(),
1371
+ updateRoutePage: vi.fn(),
1372
+ unregisterRoutePage: vi.fn(),
1373
+ },
1374
+ );
1375
+ let navigateTo: (path: string) => void = () => {};
1376
+ const CaptureNavigate = () => {
1377
+ const navigate = useNavigate();
1378
+ React.useEffect(() => {
1379
+ navigateTo = navigate;
1380
+ }, [navigate]);
1381
+ return null;
1382
+ };
1383
+
1384
+ render(
1385
+ <FlowEngineProvider engine={engine}>
1386
+ <MemoryRouter initialEntries={['/admin/371750686228480']}>
1387
+ <CaptureNavigate />
1388
+ <Routes>
1389
+ <Route path="/admin/:name" element={<FlowRoute />} />
1390
+ </Routes>
1391
+ </MemoryRouter>
1392
+ </FlowEngineProvider>,
1393
+ );
1394
+
1395
+ await waitFor(() => {
1396
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page-1', expect.any(Object));
1397
+ });
1398
+
1399
+ act(() => {
1400
+ navigateTo('/admin/371750755434496');
1401
+ });
1402
+
1403
+ await waitFor(() => {
1404
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page-2', expect.any(Object));
1405
+ });
1406
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750755434496', expect.any(Object));
1407
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
1408
+ });
1409
+
1410
+ it('should keep rendering 404 when current admin route matches no accessible schema uid or id', async () => {
1411
+ const engine = new FlowEngine();
1412
+ engine.context.defineProperty('routeRepository', {
1413
+ value: {
1414
+ refreshAccessible: hookState.refresh,
1415
+ isAccessibleLoaded: () => true,
1416
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1417
+ getRouteBySchemaUid: vi.fn(() => undefined),
1418
+ listAccessible: vi.fn(() => [
1419
+ {
1420
+ id: 371750686228480,
1421
+ schemaUid: 'group-schema',
1422
+ title: '789',
1423
+ type: NocoBaseDesktopRouteType.group,
1424
+ },
1425
+ ]),
1426
+ },
1427
+ });
1428
+ engine.context.defineProperty('app', {
1429
+ value: {
1430
+ getPublicPath: () => '/v2/',
1431
+ router: {
1432
+ getBasename: () => '/v2',
1433
+ },
1434
+ },
1435
+ });
1436
+
1437
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1438
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1439
+ {
1440
+ registerRoutePage: vi.fn(),
1441
+ updateRoutePage: vi.fn(),
1442
+ unregisterRoutePage: vi.fn(),
1443
+ },
1444
+ );
1445
+
1446
+ render(
1447
+ <FlowEngineProvider engine={engine}>
1448
+ <MemoryRouter initialEntries={['/admin/not-exists-4822']}>
1449
+ <Routes>
1450
+ <Route path="/admin/:name" element={<FlowRoute />} />
1451
+ </Routes>
1452
+ </MemoryRouter>
1453
+ </FlowEngineProvider>,
1454
+ );
1455
+
1456
+ expect(await screen.findByText('404')).toBeInTheDocument();
1457
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1458
+ });
1459
+
1460
+ it('should keep rendering 404 when current admin route matches only a non-group route id', async () => {
1461
+ const engine = new FlowEngine();
1462
+ engine.context.defineProperty('routeRepository', {
1463
+ value: {
1464
+ refreshAccessible: hookState.refresh,
1465
+ isAccessibleLoaded: () => true,
1466
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1467
+ getRouteBySchemaUid: vi.fn(() => undefined),
1468
+ listAccessible: vi.fn(() => [
1469
+ {
1470
+ id: 10001,
1471
+ schemaUid: 'flow-page-schema',
1472
+ title: 'Flow page',
1473
+ type: NocoBaseDesktopRouteType.flowPage,
1474
+ },
1475
+ ]),
1476
+ },
1477
+ });
1478
+ engine.context.defineProperty('app', {
1479
+ value: {
1480
+ getPublicPath: () => '/v2/',
1481
+ router: {
1482
+ getBasename: () => '/v2',
1483
+ },
1484
+ },
1485
+ });
1486
+
1487
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1488
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1489
+ {
1490
+ registerRoutePage: vi.fn(),
1491
+ updateRoutePage: vi.fn(),
1492
+ unregisterRoutePage: vi.fn(),
1493
+ },
1494
+ );
1495
+
1496
+ render(
1497
+ <FlowEngineProvider engine={engine}>
1498
+ <MemoryRouter initialEntries={['/admin/10001']}>
1499
+ <Routes>
1500
+ <Route path="/admin/:name" element={<FlowRoute />} />
1501
+ </Routes>
1502
+ </MemoryRouter>
1503
+ </FlowEngineProvider>,
1504
+ );
1505
+
1506
+ expect(await screen.findByText('404')).toBeInTheDocument();
1507
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1508
+ });
1509
+
1510
+ it('should keep rendering 404 when current admin route matches only a group schema uid', async () => {
1511
+ const engine = new FlowEngine();
1512
+ const groupRoute = {
1513
+ id: 371750686228480,
1514
+ schemaUid: 'group-schema',
1515
+ title: '789',
1516
+ type: NocoBaseDesktopRouteType.group,
1517
+ };
1518
+ engine.context.defineProperty('routeRepository', {
1519
+ value: {
1520
+ refreshAccessible: hookState.refresh,
1521
+ isAccessibleLoaded: () => true,
1522
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1523
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'group-schema' ? groupRoute : undefined)),
1524
+ listAccessible: vi.fn(() => [groupRoute]),
1525
+ },
1526
+ });
1527
+ engine.context.defineProperty('app', {
1528
+ value: {
1529
+ getPublicPath: () => '/v2/',
1530
+ router: {
1531
+ getBasename: () => '/v2',
1532
+ },
1533
+ },
1534
+ });
1535
+
1536
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1537
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1538
+ {
1539
+ registerRoutePage: vi.fn(),
1540
+ updateRoutePage: vi.fn(),
1541
+ unregisterRoutePage: vi.fn(),
1542
+ },
1543
+ );
1544
+
1545
+ render(
1546
+ <FlowEngineProvider engine={engine}>
1547
+ <MemoryRouter initialEntries={['/admin/group-schema']}>
1548
+ <Routes>
1549
+ <Route path="/admin/:name" element={<FlowRoute />} />
1550
+ </Routes>
1551
+ </MemoryRouter>
1552
+ </FlowEngineProvider>,
1553
+ );
1554
+
1555
+ expect(await screen.findByText('404')).toBeInTheDocument();
1556
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1557
+ });
1558
+
896
1559
  it('should not use flowModels:findOne to bridge a missing route for protected layouts', async () => {
897
1560
  const engine = new FlowEngine();
898
1561
  const findOne = vi.fn().mockResolvedValue({
@@ -1121,7 +1784,7 @@ describe('FlowRoute', () => {
1121
1784
  expect(engine.getModel('public-form-1')).toBeUndefined();
1122
1785
  });
1123
1786
 
1124
- it('should not skip accessible route loading just because layout authCheck is false', async () => {
1787
+ it('should skip accessible route loading when layout authCheck is false', async () => {
1125
1788
  const engine = new FlowEngine();
1126
1789
  const ensureAccessibleLoaded = vi.fn().mockRejectedValue(new Error('cannot load accessible routes'));
1127
1790
  const getRouteBySchemaUid = vi.fn();
@@ -1190,7 +1853,7 @@ describe('FlowRoute', () => {
1190
1853
  await waitFor(() => {
1191
1854
  expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
1192
1855
  });
1193
- expect(ensureAccessibleLoaded).toHaveBeenCalledTimes(1);
1856
+ expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
1194
1857
  expect(getRouteBySchemaUid).not.toHaveBeenCalled();
1195
1858
  });
1196
1859
 
@@ -1256,7 +1919,7 @@ describe('FlowRoute', () => {
1256
1919
  }
1257
1920
  });
1258
1921
 
1259
- it('should render not found without redirecting when admin route does not exist', async () => {
1922
+ it('should render 404 when route and FlowModel do not exist', async () => {
1260
1923
  const originalLocation = window.location;
1261
1924
  const replace = vi.fn();
1262
1925
  Object.defineProperty(window, 'location', {