@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.10

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 (334) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/PluginSettingsManager.d.ts +33 -0
  4. package/es/RouteRepository.d.ts +29 -6
  5. package/es/RouterManager.d.ts +15 -0
  6. package/es/authRedirect.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  8. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  9. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  10. package/es/components/category-tabs/index.d.ts +9 -0
  11. package/es/components/form/JsonTextArea.d.ts +2 -1
  12. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  13. package/es/components/form/TypedVariableInput.d.ts +31 -5
  14. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  15. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  16. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  17. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  18. package/es/components/form/filter/index.d.ts +2 -0
  19. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  20. package/es/components/form/index.d.ts +1 -0
  21. package/es/components/index.d.ts +2 -0
  22. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  23. package/es/entry-actions/index.d.ts +9 -0
  24. package/es/flow/actions/afterSuccess.d.ts +8 -1
  25. package/es/flow/actions/index.d.ts +2 -2
  26. package/es/flow/actions/linkageRules.d.ts +2 -0
  27. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  28. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  29. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  30. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  31. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
  32. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  33. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  34. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  35. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  36. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  37. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  38. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  39. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  40. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  41. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  42. package/es/flow/components/FlowRoute.d.ts +2 -2
  43. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  44. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  45. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  46. package/es/flow/index.d.ts +1 -0
  47. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  48. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  49. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  50. package/es/flow/models/base/GridModel.d.ts +1 -1
  51. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  52. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  53. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  54. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  55. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  56. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  57. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  58. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  59. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  60. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  61. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  62. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  63. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  64. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  65. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  66. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  67. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  68. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  69. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  70. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  71. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  72. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  73. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  74. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  75. package/es/flow/utils/formValueDeps.d.ts +32 -0
  76. package/es/flow-compat/FieldValidation.d.ts +2 -1
  77. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  78. package/es/flow-compat/routeTypes.d.ts +1 -0
  79. package/es/index.d.ts +8 -1
  80. package/es/index.mjs +367 -181
  81. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  82. package/es/utils/index.d.ts +1 -0
  83. package/es/utils/markdownSanitize.d.ts +11 -0
  84. package/lib/index.js +367 -181
  85. package/lib/locale/languageCodes.js +3 -1
  86. package/package.json +9 -7
  87. package/src/APIClient.ts +1 -10
  88. package/src/Application.tsx +4 -0
  89. package/src/BaseApplication.tsx +13 -6
  90. package/src/PluginSettingsManager.ts +53 -0
  91. package/src/RouteRepository.ts +151 -24
  92. package/src/RouterManager.tsx +142 -1
  93. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  94. package/src/__tests__/RouteRepository.test.ts +239 -0
  95. package/src/__tests__/RouterManager.test.ts +125 -0
  96. package/src/__tests__/app.test.tsx +73 -0
  97. package/src/__tests__/authRedirect.test.ts +17 -0
  98. package/src/__tests__/browserChecker.test.ts +61 -0
  99. package/src/__tests__/exports.test.ts +16 -0
  100. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  101. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +234 -2
  102. package/src/__tests__/plugin-manager.test.tsx +44 -2
  103. package/src/__tests__/settings-center.test.tsx +40 -1
  104. package/src/authRedirect.ts +38 -0
  105. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  106. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  107. package/src/collection-manager/field-configure.ts +1 -1
  108. package/src/collection-manager/field-validation.ts +1 -1
  109. package/src/collection-manager/interfaces/id.ts +1 -1
  110. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  111. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  112. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  113. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  114. package/src/collection-manager/interfaces/obo.tsx +2 -2
  115. package/src/collection-manager/interfaces/oho.tsx +2 -2
  116. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  117. package/src/collection-manager/interfaces/uuid.ts +1 -1
  118. package/src/components/AppComponents.tsx +19 -3
  119. package/src/components/README.md +7 -1
  120. package/src/components/README.zh-CN.md +6 -1
  121. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  122. package/src/components/category-tabs/index.ts +10 -0
  123. package/src/components/form/JsonTextArea.tsx +25 -11
  124. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  125. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  126. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  127. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  128. package/src/components/form/TypedVariableInput.tsx +452 -101
  129. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  130. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  131. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  132. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  133. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  134. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  135. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  136. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  137. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  138. package/src/components/form/filter/index.ts +2 -0
  139. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  140. package/src/components/form/index.tsx +1 -0
  141. package/src/components/index.ts +2 -0
  142. package/src/entry-actions/EntryActionManager.ts +76 -0
  143. package/src/entry-actions/index.ts +10 -0
  144. package/src/flow/FlowPage.tsx +38 -3
  145. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  146. package/src/flow/__tests__/FlowRoute.test.tsx +1181 -80
  147. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  148. package/src/flow/__tests__/getKey.test.ts +7 -0
  149. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  150. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +115 -0
  151. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  152. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  153. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1124 -0
  154. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  155. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  156. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  157. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  158. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  159. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  160. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  161. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  162. package/src/flow/actions/afterSuccess.tsx +142 -3
  163. package/src/flow/actions/customVariable.tsx +1 -2
  164. package/src/flow/actions/dataScopeFilter.ts +20 -1
  165. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  166. package/src/flow/actions/index.ts +3 -1
  167. package/src/flow/actions/linkageRules.tsx +209 -47
  168. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  169. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  170. package/src/flow/actions/openView.tsx +59 -5
  171. package/src/flow/actions/runjs.tsx +2 -14
  172. package/src/flow/actions/validation.tsx +62 -30
  173. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  174. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  175. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  176. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -14
  177. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  178. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  179. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  180. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  181. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  182. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  183. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  184. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  185. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  186. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  187. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
  188. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  189. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  190. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  191. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  192. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  193. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  194. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  195. package/src/flow/common/Markdown/Display.tsx +14 -3
  196. package/src/flow/common/Markdown/Edit.tsx +19 -7
  197. package/src/flow/common/Markdown/style.ts +4 -0
  198. package/src/flow/components/BlockItemCard.tsx +2 -2
  199. package/src/flow/components/DefaultValue.tsx +1 -2
  200. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  201. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  202. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  203. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  204. package/src/flow/components/FlowRoute.tsx +256 -53
  205. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  206. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  207. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  208. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  209. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  210. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  211. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  212. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  213. package/src/flow/components/fieldAssignOptions.ts +155 -27
  214. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  215. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  216. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  217. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  218. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  219. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  220. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  221. package/src/flow/index.ts +4 -1
  222. package/src/flow/internal/components/Markdown/util.ts +2 -1
  223. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  224. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  225. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  226. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  227. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  228. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  229. package/src/flow/models/base/ActionModel.tsx +21 -8
  230. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  231. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  232. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  233. package/src/flow/models/base/GridModel.tsx +38 -7
  234. package/src/flow/models/base/PageModel/PageModel.tsx +387 -50
  235. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  236. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  237. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +817 -13
  238. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  239. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  240. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  241. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  242. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  243. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  244. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  245. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  246. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  247. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +59 -6
  248. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  249. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  250. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  251. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +143 -4
  252. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  253. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  254. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  255. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  256. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  257. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  258. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  259. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  260. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  261. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  262. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  263. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  264. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  265. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  266. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  267. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  268. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  269. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  270. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +38 -14
  271. package/src/flow/models/blocks/table/TableBlockModel.tsx +42 -19
  272. package/src/flow/models/blocks/table/TableColumnModel.tsx +72 -6
  273. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  274. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  275. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  276. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  277. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  278. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +243 -1
  279. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  280. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  281. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  282. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  283. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +31 -7
  284. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  285. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  286. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  287. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  288. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  289. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  290. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  291. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  292. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  293. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  294. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  295. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  296. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  297. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  298. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  299. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  300. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  301. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  302. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  303. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  304. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  305. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  306. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  307. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  308. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  309. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  310. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  311. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  312. package/src/flow/routeTransientInputArgs.ts +27 -0
  313. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  314. package/src/flow/utils/dataScopeFormValueClear.ts +219 -81
  315. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  316. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  317. package/src/flow/utils/formValueDeps.ts +170 -0
  318. package/src/flow-compat/FieldValidation.tsx +122 -60
  319. package/src/flow-compat/Popover.tsx +43 -4
  320. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  321. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  322. package/src/flow-compat/routeTypes.ts +1 -0
  323. package/src/index.ts +14 -1
  324. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  325. package/src/locale/languageCodes.ts +3 -1
  326. package/src/nocobase-buildin-plugin/index.tsx +63 -15
  327. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  328. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  329. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  330. package/src/settings-center/plugin-manager/index.tsx +3 -0
  331. package/src/settings-center/utils.tsx +0 -6
  332. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  333. package/src/utils/index.tsx +1 -0
  334. package/src/utils/markdownSanitize.ts +88 -0
@@ -9,10 +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
+ import { RouteRepository } from '../../RouteRepository';
17
+ import { NocoBaseDesktopRouteType } from '../../flow-compat';
16
18
 
17
19
  type MockAdminLayoutModel = FlowModel & {
18
20
  registerRoutePage: ReturnType<typeof vi.fn>;
@@ -20,6 +22,8 @@ type MockAdminLayoutModel = FlowModel & {
20
22
  unregisterRoutePage: ReturnType<typeof vi.fn>;
21
23
  };
22
24
 
25
+ type RouterNavigator = React.ComponentProps<typeof Router>['navigator'];
26
+
23
27
  const { hookState } = vi.hoisted(() => {
24
28
  return {
25
29
  hookState: {
@@ -112,6 +116,7 @@ describe('FlowRoute', () => {
112
116
  }),
113
117
  );
114
118
  });
119
+ expect(engine.context.deviceType).toBe('computer');
115
120
 
116
121
  rerender(
117
122
  <FlowEngineProvider engine={engine}>
@@ -321,6 +326,94 @@ describe('FlowRoute', () => {
321
326
  });
322
327
  });
323
328
 
329
+ it('should ensure accessible routes for the current layout before bridging', async () => {
330
+ const engine = new FlowEngine();
331
+ const request = vi.fn().mockResolvedValue({
332
+ data: {
333
+ data: [
334
+ {
335
+ type: 'flowPage',
336
+ schemaUid: 'mobile-page',
337
+ },
338
+ ],
339
+ },
340
+ });
341
+ const routeRepository = new RouteRepository({
342
+ api: {
343
+ request,
344
+ resource: vi.fn(),
345
+ },
346
+ } as never);
347
+ routeRepository.setRoutes([], 'admin-layout-model');
348
+ const deactivateLayout = routeRepository.activateLayout({
349
+ uid: 'mobile-layout-model',
350
+ });
351
+ engine.context.defineProperty('routeRepository', {
352
+ value: routeRepository,
353
+ });
354
+ engine.context.defineProperty('app', {
355
+ value: {
356
+ getPublicPath: () => '/v2/',
357
+ router: {
358
+ getBasename: () => '/v2',
359
+ },
360
+ },
361
+ });
362
+ const routeModel = engine.createModel({
363
+ uid: 'mobile-route-model',
364
+ use: 'FlowModel',
365
+ });
366
+ routeModel.context.defineProperty('layout', {
367
+ value: {
368
+ routeName: 'mobile',
369
+ routePath: '/mobile',
370
+ rootRouteName: 'mobile',
371
+ uid: 'mobile-layout-model',
372
+ layoutModelClass: 'MobileLayoutModel',
373
+ rootPageModelClass: 'MobileRootPageModel',
374
+ childPageModelClass: 'MobileChildPageModel',
375
+ authCheck: true,
376
+ },
377
+ });
378
+
379
+ const mobileLayoutModel: MockAdminLayoutModel = Object.assign(
380
+ engine.createModel({
381
+ uid: 'mobile-layout-model',
382
+ use: 'FlowModel',
383
+ }),
384
+ {
385
+ registerRoutePage: vi.fn(),
386
+ updateRoutePage: vi.fn(),
387
+ unregisterRoutePage: vi.fn(),
388
+ },
389
+ );
390
+
391
+ render(
392
+ <FlowEngineProvider engine={engine}>
393
+ <FlowContextProvider context={routeModel.context}>
394
+ <MemoryRouter initialEntries={['/mobile/mobile-page']}>
395
+ <Routes>
396
+ <Route path="/mobile/:name" element={<FlowRoute />} />
397
+ </Routes>
398
+ </MemoryRouter>
399
+ </FlowContextProvider>
400
+ </FlowEngineProvider>,
401
+ );
402
+
403
+ await waitFor(() => {
404
+ expect(request).toHaveBeenCalledWith({
405
+ url: '/desktopRoutes:listAccessible',
406
+ params: {
407
+ tree: true,
408
+ sort: 'sort',
409
+ layout: 'mobile-layout-model',
410
+ },
411
+ });
412
+ expect(mobileLayoutModel.registerRoutePage).toHaveBeenCalledWith('mobile-page', expect.any(Object));
413
+ });
414
+ deactivateLayout();
415
+ });
416
+
324
417
  it('should fail fast when admin-layout-model is missing', () => {
325
418
  const engine = new FlowEngine();
326
419
  engine.context.defineProperty('route', {
@@ -420,14 +513,83 @@ describe('FlowRoute', () => {
420
513
  });
421
514
  });
422
515
 
423
- it('should show 404 when current route is a legacy page in v2 runtime', async () => {
516
+ it('should not rerun accessible guard when layout reference changes but fields stay the same', async () => {
517
+ const engine = new FlowEngine();
518
+ const ensureAccessibleLoaded = vi.fn().mockResolvedValue([]);
519
+ const getRouteBySchemaUid = vi.fn(() => ({ type: 'flowPage', schemaUid: 'test-page' }));
520
+ engine.context.defineProperty('routeRepository', {
521
+ value: {
522
+ refreshAccessible: hookState.refresh,
523
+ isAccessibleLoaded: () => true,
524
+ ensureAccessibleLoaded,
525
+ getRouteBySchemaUid,
526
+ },
527
+ });
528
+ engine.context.defineProperty('app', {
529
+ value: {
530
+ getPublicPath: () => '/v2/',
531
+ router: {
532
+ getBasename: () => '/v2',
533
+ },
534
+ },
535
+ });
536
+
537
+ const layoutModel: MockAdminLayoutModel = Object.assign(
538
+ engine.createModel({ uid: 'legacy-admin-layout-model', use: 'FlowModel' }),
539
+ {
540
+ registerRoutePage: vi.fn(),
541
+ updateRoutePage: vi.fn(),
542
+ unregisterRoutePage: vi.fn(),
543
+ },
544
+ );
545
+ let layoutReadCount = 0;
546
+ const stableLayout = {
547
+ uid: 'admin-layout-model',
548
+ routeName: 'admin',
549
+ routePath: '/admin',
550
+ authCheck: true,
551
+ };
552
+ Object.defineProperty(layoutModel, 'layout', {
553
+ get: () => {
554
+ layoutReadCount += 1;
555
+ return layoutReadCount <= 2 ? { ...stableLayout } : stableLayout;
556
+ },
557
+ });
558
+
559
+ const element = (
560
+ <FlowEngineProvider engine={engine}>
561
+ <MemoryRouter initialEntries={['/flow/test-page']}>
562
+ <Routes>
563
+ <Route path="/flow/:name" element={<FlowRoute getLayoutModel={() => layoutModel as any} />} />
564
+ </Routes>
565
+ </MemoryRouter>
566
+ </FlowEngineProvider>
567
+ );
568
+ const { rerender } = render(element);
569
+
570
+ await waitFor(() => {
571
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('test-page', expect.any(Object));
572
+ });
573
+ expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
574
+ expect(getRouteBySchemaUid).toHaveBeenCalledTimes(1);
575
+
576
+ rerender(element);
577
+
578
+ await waitFor(() => {
579
+ expect(layoutModel.updateRoutePage).toHaveBeenCalled();
580
+ });
581
+ expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
582
+ expect(getRouteBySchemaUid).toHaveBeenCalledTimes(1);
583
+ });
584
+
585
+ it('should explain how to open a legacy page outside the v2 runtime', async () => {
424
586
  const originalLocation = window.location;
425
587
  const replace = vi.fn();
426
588
  Object.defineProperty(window, 'location', {
427
589
  configurable: true,
428
590
  value: {
429
591
  ...originalLocation,
430
- pathname: '/v2/admin/test-page/tab/tab-1',
592
+ pathname: '/nocobase/v2/apps/jhb20/admin/test-page/tab/tab-1',
431
593
  search: '?from=direct',
432
594
  hash: '#dialog',
433
595
  replace,
@@ -446,9 +608,9 @@ describe('FlowRoute', () => {
446
608
  });
447
609
  engine.context.defineProperty('app', {
448
610
  value: {
449
- getPublicPath: () => '/v2/',
611
+ getPublicPath: () => '/nocobase/v2/',
450
612
  router: {
451
- getBasename: () => '/v2',
613
+ getBasename: () => '/nocobase/v2',
452
614
  },
453
615
  },
454
616
  });
@@ -464,15 +626,25 @@ describe('FlowRoute', () => {
464
626
 
465
627
  render(
466
628
  <FlowEngineProvider engine={engine}>
467
- <MemoryRouter initialEntries={['/flow/test-page']}>
629
+ <MemoryRouter initialEntries={['/nocobase/v2/apps/jhb20/admin/test-page/tab/tab-1?from=direct#dialog']}>
468
630
  <Routes>
469
- <Route path="/flow/:name" element={<FlowRoute />} />
631
+ <Route path="/nocobase/v2/apps/jhb20/admin/:name/*" element={<FlowRoute />} />
470
632
  </Routes>
471
633
  </MemoryRouter>
472
634
  </FlowEngineProvider>,
473
635
  );
474
636
 
475
- 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
+ );
476
648
  expect(replace).not.toHaveBeenCalled();
477
649
  expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
478
650
  expect(adminLayoutModel.updateRoutePage).not.toHaveBeenCalled();
@@ -545,24 +717,42 @@ describe('FlowRoute', () => {
545
717
  }
546
718
  });
547
719
 
548
- it('should bridge existing FlowModel when behavior is notFound and routeRepository has no route', async () => {
720
+ it('should render 404 for missing FlowModel in notFound mode without routeRepository', async () => {
549
721
  const engine = new FlowEngine();
550
- engine.setModelRepository({
551
- findOne: vi.fn().mockResolvedValue({
552
- uid: 'public-form-1',
553
- use: 'FlowModel',
554
- }),
555
- save: vi.fn(),
556
- destroy: vi.fn(),
557
- } as any);
558
- engine.context.defineProperty('routeRepository', {
722
+ engine.context.defineProperty('app', {
559
723
  value: {
560
- refreshAccessible: hookState.refresh,
561
- isAccessibleLoaded: () => true,
562
- ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
563
- getRouteBySchemaUid: vi.fn(() => undefined),
724
+ getPublicPath: () => '/v2/',
725
+ router: {
726
+ getBasename: () => '/v2',
727
+ },
564
728
  },
565
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();
566
756
  engine.context.defineProperty('app', {
567
757
  value: {
568
758
  getPublicPath: () => '/v2/',
@@ -572,8 +762,8 @@ describe('FlowRoute', () => {
572
762
  },
573
763
  });
574
764
 
575
- const layoutModel: MockAdminLayoutModel = Object.assign(
576
- engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
765
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
766
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
577
767
  {
578
768
  registerRoutePage: vi.fn(),
579
769
  updateRoutePage: vi.fn(),
@@ -583,37 +773,64 @@ describe('FlowRoute', () => {
583
773
 
584
774
  render(
585
775
  <FlowEngineProvider engine={engine}>
586
- <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
776
+ <MemoryRouter initialEntries={['/flow/missing-page']}>
587
777
  <Routes>
588
- <Route
589
- path="/public-forms/:name"
590
- element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
591
- />
778
+ <Route path="/flow/:name" element={<FlowRoute />} />
592
779
  </Routes>
593
780
  </MemoryRouter>
594
781
  </FlowEngineProvider>,
595
782
  );
596
783
 
597
784
  await waitFor(() => {
598
- expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
785
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('missing-page', expect.any(Object));
599
786
  });
600
787
  expect(screen.queryByText('404')).not.toBeInTheDocument();
601
788
  });
602
789
 
603
- it('should check model existence without occupying the route model uid', async () => {
790
+ it('should bridge existing FlowModel in notFound mode without routeRepository', async () => {
604
791
  const engine = new FlowEngine();
605
- const findOne = vi.fn().mockResolvedValue({
606
- uid: 'public-form-1',
607
- });
608
- const request = vi.fn().mockResolvedValue({
609
- data: {
610
- data: {
611
- uid: 'public-form-1',
792
+ engine.createModel({ uid: 'test-page', use: 'FlowModel' });
793
+ engine.context.defineProperty('app', {
794
+ value: {
795
+ getPublicPath: () => '/v2/',
796
+ router: {
797
+ getBasename: () => '/v2',
612
798
  },
613
799
  },
614
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
+
827
+ it('should bridge existing FlowModel when behavior is notFound and routeRepository has no route', async () => {
828
+ const engine = new FlowEngine();
615
829
  engine.setModelRepository({
616
- findOne,
830
+ findOne: vi.fn().mockResolvedValue({
831
+ uid: 'public-form-1',
832
+ use: 'FlowModel',
833
+ }),
617
834
  save: vi.fn(),
618
835
  destroy: vi.fn(),
619
836
  } as any);
@@ -627,9 +844,6 @@ describe('FlowRoute', () => {
627
844
  });
628
845
  engine.context.defineProperty('app', {
629
846
  value: {
630
- apiClient: {
631
- request,
632
- },
633
847
  getPublicPath: () => '/v2/',
634
848
  router: {
635
849
  getBasename: () => '/v2',
@@ -667,7 +881,10 @@ describe('FlowRoute', () => {
667
881
  <FlowContextProvider context={routeModel.context}>
668
882
  <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
669
883
  <Routes>
670
- <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
884
+ <Route
885
+ path="/public-forms/:name"
886
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
887
+ />
671
888
  </Routes>
672
889
  </MemoryRouter>
673
890
  </FlowContextProvider>
@@ -677,15 +894,11 @@ describe('FlowRoute', () => {
677
894
  await waitFor(() => {
678
895
  expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
679
896
  });
680
- expect(findOne).toHaveBeenCalledWith({ uid: 'public-form-1' });
681
- expect(request).not.toHaveBeenCalled();
682
- expect(engine.getModel('public-form-1')).toBeUndefined();
897
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
683
898
  });
684
899
 
685
- it('should not skip accessible route loading just because layout authCheck is false', async () => {
900
+ it('should use getLayoutModel layout authCheck when FlowContext has no layout', async () => {
686
901
  const engine = new FlowEngine();
687
- const ensureAccessibleLoaded = vi.fn().mockRejectedValue(new Error('cannot load accessible routes'));
688
- const getRouteBySchemaUid = vi.fn();
689
902
  engine.setModelRepository({
690
903
  findOne: vi.fn().mockResolvedValue({
691
904
  uid: 'public-form-1',
@@ -697,9 +910,9 @@ describe('FlowRoute', () => {
697
910
  engine.context.defineProperty('routeRepository', {
698
911
  value: {
699
912
  refreshAccessible: hookState.refresh,
700
- isAccessibleLoaded: () => false,
701
- ensureAccessibleLoaded,
702
- getRouteBySchemaUid,
913
+ isAccessibleLoaded: () => true,
914
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
915
+ getRouteBySchemaUid: vi.fn(() => undefined),
703
916
  },
704
917
  });
705
918
  engine.context.defineProperty('app', {
@@ -710,11 +923,16 @@ describe('FlowRoute', () => {
710
923
  },
711
924
  },
712
925
  });
713
- const routeModel = engine.createModel({
714
- uid: 'public-form-route-model',
715
- use: 'FlowModel',
716
- });
717
- routeModel.context.defineProperty('layout', {
926
+
927
+ const layoutModel: MockAdminLayoutModel = Object.assign(
928
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
929
+ {
930
+ registerRoutePage: vi.fn(),
931
+ updateRoutePage: vi.fn(),
932
+ unregisterRoutePage: vi.fn(),
933
+ },
934
+ );
935
+ Object.defineProperty(layoutModel, 'layout', {
718
936
  value: {
719
937
  routeName: 'public-forms',
720
938
  routePath: '/public-forms',
@@ -727,32 +945,916 @@ describe('FlowRoute', () => {
727
945
  },
728
946
  });
729
947
 
730
- const layoutModel: MockAdminLayoutModel = Object.assign(
731
- engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
732
- {
733
- registerRoutePage: vi.fn(),
734
- updateRoutePage: vi.fn(),
735
- unregisterRoutePage: vi.fn(),
736
- },
737
- );
738
-
739
948
  render(
740
949
  <FlowEngineProvider engine={engine}>
741
- <FlowContextProvider context={routeModel.context}>
742
- <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
743
- <Routes>
744
- <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
745
- </Routes>
746
- </MemoryRouter>
747
- </FlowContextProvider>
950
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
951
+ <Routes>
952
+ <Route
953
+ path="/public-forms/:name"
954
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel as any} />}
955
+ />
956
+ </Routes>
957
+ </MemoryRouter>
748
958
  </FlowEngineProvider>,
749
959
  );
750
960
 
751
961
  await waitFor(() => {
752
962
  expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
753
963
  });
754
- expect(ensureAccessibleLoaded).toHaveBeenCalledTimes(1);
755
- expect(getRouteBySchemaUid).not.toHaveBeenCalled();
964
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
965
+ });
966
+
967
+ it('should render not found when admin route is not accessible', async () => {
968
+ const engine = new FlowEngine();
969
+ engine.setModelRepository({
970
+ findOne: vi.fn().mockResolvedValue({
971
+ uid: 'admin-denied-page',
972
+ use: 'FlowModel',
973
+ }),
974
+ save: vi.fn(),
975
+ destroy: vi.fn(),
976
+ } as any);
977
+ engine.context.defineProperty('routeRepository', {
978
+ value: {
979
+ refreshAccessible: hookState.refresh,
980
+ isAccessibleLoaded: () => true,
981
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
982
+ getRouteBySchemaUid: vi.fn(() => undefined),
983
+ },
984
+ });
985
+ engine.context.defineProperty('app', {
986
+ value: {
987
+ getPublicPath: () => '/v2/',
988
+ router: {
989
+ getBasename: () => '/v2',
990
+ },
991
+ },
992
+ });
993
+
994
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
995
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
996
+ {
997
+ registerRoutePage: vi.fn(),
998
+ updateRoutePage: vi.fn(),
999
+ unregisterRoutePage: vi.fn(),
1000
+ },
1001
+ );
1002
+
1003
+ render(
1004
+ <FlowEngineProvider engine={engine}>
1005
+ <MemoryRouter initialEntries={['/admin/admin-denied-page']}>
1006
+ <Routes>
1007
+ <Route path="/admin/:name" element={<FlowRoute />} />
1008
+ </Routes>
1009
+ </MemoryRouter>
1010
+ </FlowEngineProvider>,
1011
+ );
1012
+
1013
+ expect(await screen.findByText('404')).toBeInTheDocument();
1014
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1015
+ });
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
+
1559
+ it('should not use flowModels:findOne to bridge a missing route for protected layouts', async () => {
1560
+ const engine = new FlowEngine();
1561
+ const findOne = vi.fn().mockResolvedValue({
1562
+ uid: 'mobile-denied-page',
1563
+ use: 'FlowModel',
1564
+ });
1565
+ engine.setModelRepository({
1566
+ findOne,
1567
+ save: vi.fn(),
1568
+ destroy: vi.fn(),
1569
+ } as any);
1570
+ engine.context.defineProperty('routeRepository', {
1571
+ value: {
1572
+ refreshAccessible: hookState.refresh,
1573
+ isAccessibleLoaded: () => true,
1574
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1575
+ getRouteBySchemaUid: vi.fn(() => undefined),
1576
+ },
1577
+ });
1578
+ engine.context.defineProperty('app', {
1579
+ value: {
1580
+ getPublicPath: () => '/v2/',
1581
+ router: {
1582
+ getBasename: () => '/v2',
1583
+ },
1584
+ },
1585
+ });
1586
+ const routeModel = engine.createModel({
1587
+ uid: 'mobile-route-model',
1588
+ use: 'FlowModel',
1589
+ });
1590
+ routeModel.context.defineProperty('layout', {
1591
+ value: {
1592
+ routeName: 'mobile',
1593
+ routePath: '/mobile',
1594
+ rootRouteName: 'mobile',
1595
+ uid: 'mobile-layout-model',
1596
+ layoutModelClass: 'MobileLayoutModel',
1597
+ rootPageModelClass: 'MobileRootPageModel',
1598
+ childPageModelClass: 'MobileChildPageModel',
1599
+ authCheck: true,
1600
+ },
1601
+ });
1602
+
1603
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1604
+ engine.createModel({ uid: 'mobile-layout-model', use: 'FlowModel' }),
1605
+ {
1606
+ registerRoutePage: vi.fn(),
1607
+ updateRoutePage: vi.fn(),
1608
+ unregisterRoutePage: vi.fn(),
1609
+ },
1610
+ );
1611
+
1612
+ render(
1613
+ <FlowEngineProvider engine={engine}>
1614
+ <FlowContextProvider context={routeModel.context}>
1615
+ <MemoryRouter initialEntries={['/mobile/mobile-denied-page']}>
1616
+ <Routes>
1617
+ <Route
1618
+ path="/mobile/:name"
1619
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
1620
+ />
1621
+ </Routes>
1622
+ </MemoryRouter>
1623
+ </FlowContextProvider>
1624
+ </FlowEngineProvider>,
1625
+ );
1626
+
1627
+ expect(await screen.findByText('404')).toBeInTheDocument();
1628
+ expect(findOne).not.toHaveBeenCalled();
1629
+ expect(layoutModel.registerRoutePage).not.toHaveBeenCalled();
1630
+ });
1631
+
1632
+ it('should not load a known page uid when a protected custom layout has no accessible route', async () => {
1633
+ const engine = new FlowEngine();
1634
+ const findOne = vi.fn().mockResolvedValue({
1635
+ uid: 'known-denied-custom-page',
1636
+ use: 'FlowModel',
1637
+ });
1638
+ engine.setModelRepository({
1639
+ findOne,
1640
+ save: vi.fn(),
1641
+ destroy: vi.fn(),
1642
+ } as any);
1643
+ engine.context.defineProperty('routeRepository', {
1644
+ value: {
1645
+ refreshAccessible: hookState.refresh,
1646
+ isAccessibleLoaded: () => true,
1647
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1648
+ getRouteBySchemaUid: vi.fn(() => undefined),
1649
+ },
1650
+ });
1651
+ engine.context.defineProperty('app', {
1652
+ value: {
1653
+ getPublicPath: () => '/v2/',
1654
+ router: {
1655
+ getBasename: () => '/v2',
1656
+ },
1657
+ },
1658
+ });
1659
+ const routeModel = engine.createModel({
1660
+ uid: 'custom-layout-route-model',
1661
+ use: 'FlowModel',
1662
+ });
1663
+ routeModel.context.defineProperty('layout', {
1664
+ value: {
1665
+ routeName: 'admin2',
1666
+ routePath: '/admin2',
1667
+ rootRouteName: 'admin2',
1668
+ uid: 'custom-protected-layout-model',
1669
+ layoutModelClass: 'AdminLayoutModel',
1670
+ rootPageModelClass: 'RootPageModel',
1671
+ childPageModelClass: 'ChildPageModel',
1672
+ authCheck: true,
1673
+ },
1674
+ });
1675
+
1676
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1677
+ engine.createModel({ uid: 'custom-protected-layout-model', use: 'FlowModel' }),
1678
+ {
1679
+ registerRoutePage: vi.fn(),
1680
+ updateRoutePage: vi.fn(),
1681
+ unregisterRoutePage: vi.fn(),
1682
+ },
1683
+ );
1684
+
1685
+ render(
1686
+ <FlowEngineProvider engine={engine}>
1687
+ <FlowContextProvider context={routeModel.context}>
1688
+ <MemoryRouter initialEntries={['/admin2/known-denied-custom-page']}>
1689
+ <Routes>
1690
+ <Route
1691
+ path="/admin2/:name"
1692
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
1693
+ />
1694
+ </Routes>
1695
+ </MemoryRouter>
1696
+ </FlowContextProvider>
1697
+ </FlowEngineProvider>,
1698
+ );
1699
+
1700
+ expect(await screen.findByText('404')).toBeInTheDocument();
1701
+ expect(findOne).not.toHaveBeenCalled();
1702
+ expect(layoutModel.registerRoutePage).not.toHaveBeenCalled();
1703
+ });
1704
+
1705
+ it('should check model existence without occupying the route model uid', async () => {
1706
+ const engine = new FlowEngine();
1707
+ const findOne = vi.fn().mockResolvedValue({
1708
+ uid: 'public-form-1',
1709
+ });
1710
+ const request = vi.fn().mockResolvedValue({
1711
+ data: {
1712
+ data: {
1713
+ uid: 'public-form-1',
1714
+ },
1715
+ },
1716
+ });
1717
+ engine.setModelRepository({
1718
+ findOne,
1719
+ save: vi.fn(),
1720
+ destroy: vi.fn(),
1721
+ } as any);
1722
+ engine.context.defineProperty('routeRepository', {
1723
+ value: {
1724
+ refreshAccessible: hookState.refresh,
1725
+ isAccessibleLoaded: () => true,
1726
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1727
+ getRouteBySchemaUid: vi.fn(() => undefined),
1728
+ },
1729
+ });
1730
+ engine.context.defineProperty('app', {
1731
+ value: {
1732
+ apiClient: {
1733
+ request,
1734
+ },
1735
+ getPublicPath: () => '/v2/',
1736
+ router: {
1737
+ getBasename: () => '/v2',
1738
+ },
1739
+ },
1740
+ });
1741
+ const routeModel = engine.createModel({
1742
+ uid: 'public-form-route-model',
1743
+ use: 'FlowModel',
1744
+ });
1745
+ routeModel.context.defineProperty('layout', {
1746
+ value: {
1747
+ routeName: 'public-forms',
1748
+ routePath: '/public-forms',
1749
+ rootRouteName: 'public-forms',
1750
+ uid: 'public-form-layout-model',
1751
+ layoutModelClass: 'PublicFormLayoutModel',
1752
+ rootPageModelClass: 'PublicFormPageModel',
1753
+ childPageModelClass: 'ChildPageModel',
1754
+ authCheck: false,
1755
+ },
1756
+ });
1757
+
1758
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1759
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
1760
+ {
1761
+ registerRoutePage: vi.fn(),
1762
+ updateRoutePage: vi.fn(),
1763
+ unregisterRoutePage: vi.fn(),
1764
+ },
1765
+ );
1766
+
1767
+ render(
1768
+ <FlowEngineProvider engine={engine}>
1769
+ <FlowContextProvider context={routeModel.context}>
1770
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
1771
+ <Routes>
1772
+ <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
1773
+ </Routes>
1774
+ </MemoryRouter>
1775
+ </FlowContextProvider>
1776
+ </FlowEngineProvider>,
1777
+ );
1778
+
1779
+ await waitFor(() => {
1780
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
1781
+ });
1782
+ expect(findOne).toHaveBeenCalledWith({ uid: 'public-form-1' });
1783
+ expect(request).not.toHaveBeenCalled();
1784
+ expect(engine.getModel('public-form-1')).toBeUndefined();
1785
+ });
1786
+
1787
+ it('should skip accessible route loading when layout authCheck is false', async () => {
1788
+ const engine = new FlowEngine();
1789
+ const ensureAccessibleLoaded = vi.fn().mockRejectedValue(new Error('cannot load accessible routes'));
1790
+ const getRouteBySchemaUid = vi.fn();
1791
+ engine.setModelRepository({
1792
+ findOne: vi.fn().mockResolvedValue({
1793
+ uid: 'public-form-1',
1794
+ use: 'FlowModel',
1795
+ }),
1796
+ save: vi.fn(),
1797
+ destroy: vi.fn(),
1798
+ } as any);
1799
+ engine.context.defineProperty('routeRepository', {
1800
+ value: {
1801
+ refreshAccessible: hookState.refresh,
1802
+ isAccessibleLoaded: () => false,
1803
+ ensureAccessibleLoaded,
1804
+ getRouteBySchemaUid,
1805
+ },
1806
+ });
1807
+ engine.context.defineProperty('app', {
1808
+ value: {
1809
+ getPublicPath: () => '/v2/',
1810
+ router: {
1811
+ getBasename: () => '/v2',
1812
+ },
1813
+ },
1814
+ });
1815
+ const routeModel = engine.createModel({
1816
+ uid: 'public-form-route-model',
1817
+ use: 'FlowModel',
1818
+ });
1819
+ routeModel.context.defineProperty('layout', {
1820
+ value: {
1821
+ routeName: 'public-forms',
1822
+ routePath: '/public-forms',
1823
+ rootRouteName: 'public-forms',
1824
+ uid: 'public-form-layout-model',
1825
+ layoutModelClass: 'PublicFormLayoutModel',
1826
+ rootPageModelClass: 'PublicFormPageModel',
1827
+ childPageModelClass: 'ChildPageModel',
1828
+ authCheck: false,
1829
+ },
1830
+ });
1831
+
1832
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1833
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
1834
+ {
1835
+ registerRoutePage: vi.fn(),
1836
+ updateRoutePage: vi.fn(),
1837
+ unregisterRoutePage: vi.fn(),
1838
+ },
1839
+ );
1840
+
1841
+ render(
1842
+ <FlowEngineProvider engine={engine}>
1843
+ <FlowContextProvider context={routeModel.context}>
1844
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
1845
+ <Routes>
1846
+ <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
1847
+ </Routes>
1848
+ </MemoryRouter>
1849
+ </FlowContextProvider>
1850
+ </FlowEngineProvider>,
1851
+ );
1852
+
1853
+ await waitFor(() => {
1854
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
1855
+ });
1856
+ expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
1857
+ expect(getRouteBySchemaUid).not.toHaveBeenCalled();
756
1858
  });
757
1859
 
758
1860
  it('should bridge legacy page when behavior is bridge', async () => {
@@ -817,7 +1919,7 @@ describe('FlowRoute', () => {
817
1919
  }
818
1920
  });
819
1921
 
820
- it('should not redirect when route does not exist', async () => {
1922
+ it('should render 404 when route and FlowModel do not exist', async () => {
821
1923
  const originalLocation = window.location;
822
1924
  const replace = vi.fn();
823
1925
  Object.defineProperty(window, 'location', {
@@ -866,9 +1968,8 @@ describe('FlowRoute', () => {
866
1968
  </FlowEngineProvider>,
867
1969
  );
868
1970
 
869
- await waitFor(() => {
870
- expect(adminLayoutModel.registerRoutePage).toHaveBeenCalled();
871
- });
1971
+ expect(await screen.findByText('404')).toBeInTheDocument();
1972
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
872
1973
  expect(replace).not.toHaveBeenCalled();
873
1974
  } finally {
874
1975
  Object.defineProperty(window, 'location', {