@nocobase/client-v2 2.1.0-alpha.40 → 2.1.0-alpha.45

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 (278) hide show
  1. package/es/Application.d.ts +7 -0
  2. package/es/BaseApplication.d.ts +13 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterface.d.ts +51 -15
  5. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +82 -3
  6. package/es/collection-manager/field-configure.d.ts +80 -0
  7. package/es/collection-manager/field-validation.d.ts +43 -0
  8. package/es/collection-manager/filter-operators/index.d.ts +46 -0
  9. package/es/collection-manager/filter-operators/operators.d.ts +30 -0
  10. package/es/collection-manager/interfaces/checkbox.d.ts +1 -41
  11. package/es/collection-manager/interfaces/checkboxGroup.d.ts +12 -44
  12. package/es/collection-manager/interfaces/collection.d.ts +12 -51
  13. package/es/collection-manager/interfaces/color.d.ts +1 -16
  14. package/es/collection-manager/interfaces/createdAt.d.ts +1 -44
  15. package/es/collection-manager/interfaces/createdBy.d.ts +0 -4
  16. package/es/collection-manager/interfaces/dateOnly.d.ts +7 -44
  17. package/es/collection-manager/interfaces/datetime.d.ts +1 -44
  18. package/es/collection-manager/interfaces/datetimeNoTz.d.ts +1 -44
  19. package/es/collection-manager/interfaces/email.d.ts +1 -29
  20. package/es/collection-manager/interfaces/id.d.ts +1 -16
  21. package/es/collection-manager/interfaces/index.d.ts +2 -3
  22. package/es/collection-manager/interfaces/input.d.ts +1 -102
  23. package/es/collection-manager/interfaces/integer.d.ts +1 -95
  24. package/es/collection-manager/interfaces/json.d.ts +16 -7
  25. package/es/collection-manager/interfaces/m2m.d.ts +11 -19
  26. package/es/collection-manager/interfaces/m2o.d.ts +11 -19
  27. package/es/collection-manager/interfaces/markdown.d.ts +1 -63
  28. package/es/collection-manager/interfaces/multipleSelect.d.ts +12 -44
  29. package/es/collection-manager/interfaces/nanoid.d.ts +1 -34
  30. package/es/collection-manager/interfaces/number.d.ts +1 -87
  31. package/es/collection-manager/interfaces/o2m.d.ts +12 -24
  32. package/es/collection-manager/interfaces/obo.d.ts +207 -0
  33. package/es/collection-manager/interfaces/oho.d.ts +207 -0
  34. package/es/collection-manager/interfaces/password.d.ts +1 -56
  35. package/es/collection-manager/interfaces/percent.d.ts +1 -84
  36. package/es/collection-manager/interfaces/phone.d.ts +1 -25
  37. package/es/collection-manager/interfaces/properties/index.d.ts +0 -28
  38. package/es/collection-manager/interfaces/radioGroup.d.ts +1 -29
  39. package/es/collection-manager/interfaces/richText.d.ts +1 -63
  40. package/es/collection-manager/interfaces/select.d.ts +12 -44
  41. package/es/collection-manager/interfaces/snowflake-id.d.ts +1 -34
  42. package/es/collection-manager/interfaces/tableoid.d.ts +1 -10
  43. package/es/collection-manager/interfaces/textarea.d.ts +1 -51
  44. package/es/collection-manager/interfaces/time.d.ts +1 -16
  45. package/es/collection-manager/interfaces/types.d.ts +3 -12
  46. package/es/collection-manager/interfaces/unixTimestamp.d.ts +1 -44
  47. package/es/collection-manager/interfaces/updatedAt.d.ts +1 -44
  48. package/es/collection-manager/interfaces/updatedBy.d.ts +0 -4
  49. package/es/collection-manager/interfaces/url.d.ts +1 -20
  50. package/es/collection-manager/interfaces/uuid.d.ts +1 -34
  51. package/es/collection-manager/template-fields.d.ts +53 -0
  52. package/es/components/KeepAlive.d.ts +22 -0
  53. package/es/components/RouterBridge.d.ts +9 -0
  54. package/es/components/form/DialogFormLayout.d.ts +5 -29
  55. package/es/components/form/VariableInput.d.ts +53 -2
  56. package/es/components/form/filter/CollectionFilter.d.ts +49 -0
  57. package/es/components/form/filter/CollectionFilterItem.d.ts +49 -0
  58. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  59. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  60. package/es/components/form/filter/index.d.ts +11 -0
  61. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  62. package/es/components/form/index.d.ts +1 -0
  63. package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
  64. package/es/data-source/index.d.ts +9 -0
  65. package/es/flow/FlowPage.d.ts +2 -1
  66. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  67. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  68. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  69. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  70. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  71. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
  72. package/es/flow/admin-shell/admin-layout/index.d.ts +3 -0
  73. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
  74. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  75. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  76. package/es/flow/components/FlowRoute.d.ts +10 -1
  77. package/es/flow/components/filter/index.d.ts +2 -0
  78. package/es/flow/components/filter/useFilterOptions.d.ts +66 -0
  79. package/es/flow/index.d.ts +4 -0
  80. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  81. package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +84 -0
  82. package/es/flow/models/blocks/assign-form/index.d.ts +1 -0
  83. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  84. package/es/flow/models/blocks/form/FormActionModel.d.ts +9 -2
  85. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  86. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  87. package/es/flow-compat/passwordUtils.d.ts +1 -1
  88. package/es/index.d.ts +6 -0
  89. package/es/index.mjs +552 -459
  90. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  91. package/es/layout-manager/LayoutManager.d.ts +22 -0
  92. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  93. package/es/layout-manager/index.d.ts +13 -0
  94. package/es/layout-manager/types.d.ts +20 -0
  95. package/es/layout-manager/utils.d.ts +14 -0
  96. package/es/nocobase-buildin-plugin/index.d.ts +3 -10
  97. package/es/settings-center/index.d.ts +1 -1
  98. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  99. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  100. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  101. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  102. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  103. package/lib/index.js +552 -459
  104. package/package.json +8 -7
  105. package/src/Application.tsx +51 -12
  106. package/src/BaseApplication.tsx +32 -0
  107. package/src/PluginSettingsManager.ts +1 -1
  108. package/src/RouterManager.tsx +17 -1
  109. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  110. package/src/__tests__/app.test.tsx +17 -1
  111. package/src/__tests__/globalDeps.test.ts +1 -0
  112. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  113. package/src/__tests__/plugin-manager.test.tsx +177 -0
  114. package/src/__tests__/settings-center.test.tsx +24 -2
  115. package/src/collection-field-interface/CollectionFieldInterface.ts +71 -77
  116. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +201 -4
  117. package/src/collection-manager/field-configure.ts +548 -0
  118. package/src/collection-manager/field-validation.ts +195 -0
  119. package/src/collection-manager/filter-operators/index.ts +176 -0
  120. package/src/collection-manager/{interfaces/properties → filter-operators}/operators.ts +24 -13
  121. package/src/collection-manager/interfaces/checkbox.ts +2 -9
  122. package/src/collection-manager/interfaces/checkboxGroup.ts +2 -10
  123. package/src/collection-manager/interfaces/collection.ts +2 -15
  124. package/src/collection-manager/interfaces/color.ts +2 -2
  125. package/src/collection-manager/interfaces/createdAt.ts +2 -2
  126. package/src/collection-manager/interfaces/createdBy.ts +1 -12
  127. package/src/collection-manager/interfaces/dateOnly.ts +8 -2
  128. package/src/collection-manager/interfaces/datetime.ts +2 -2
  129. package/src/collection-manager/interfaces/datetimeNoTz.ts +2 -2
  130. package/src/collection-manager/interfaces/email.ts +2 -9
  131. package/src/collection-manager/interfaces/id.ts +1 -2
  132. package/src/collection-manager/interfaces/index.ts +2 -3
  133. package/src/collection-manager/interfaces/input.ts +2 -133
  134. package/src/collection-manager/interfaces/integer.ts +2 -71
  135. package/src/collection-manager/interfaces/json.tsx +17 -11
  136. package/src/collection-manager/interfaces/m2m.tsx +0 -21
  137. package/src/collection-manager/interfaces/m2o.tsx +0 -22
  138. package/src/collection-manager/interfaces/markdown.ts +2 -51
  139. package/src/collection-manager/interfaces/multipleSelect.ts +2 -14
  140. package/src/collection-manager/interfaces/nanoid.ts +2 -2
  141. package/src/collection-manager/interfaces/number.ts +2 -85
  142. package/src/collection-manager/interfaces/o2m.tsx +1 -22
  143. package/src/collection-manager/interfaces/obo.tsx +145 -0
  144. package/src/collection-manager/interfaces/oho.tsx +145 -0
  145. package/src/collection-manager/interfaces/password.ts +2 -44
  146. package/src/collection-manager/interfaces/percent.ts +2 -74
  147. package/src/collection-manager/interfaces/phone.ts +2 -2
  148. package/src/collection-manager/interfaces/properties/index.ts +0 -133
  149. package/src/collection-manager/interfaces/radioGroup.ts +2 -2
  150. package/src/collection-manager/interfaces/richText.ts +2 -51
  151. package/src/collection-manager/interfaces/select.ts +2 -14
  152. package/src/collection-manager/interfaces/snowflake-id.ts +2 -2
  153. package/src/collection-manager/interfaces/tableoid.ts +1 -2
  154. package/src/collection-manager/interfaces/textarea.ts +2 -51
  155. package/src/collection-manager/interfaces/time.ts +2 -2
  156. package/src/collection-manager/interfaces/types.ts +4 -12
  157. package/src/collection-manager/interfaces/unixTimestamp.tsx +2 -2
  158. package/src/collection-manager/interfaces/updatedAt.ts +2 -2
  159. package/src/collection-manager/interfaces/updatedBy.ts +1 -12
  160. package/src/collection-manager/interfaces/url.ts +2 -4
  161. package/src/collection-manager/interfaces/uuid.ts +2 -2
  162. package/src/collection-manager/template-fields.ts +109 -0
  163. package/src/components/KeepAlive.tsx +131 -0
  164. package/src/components/README.md +90 -6
  165. package/src/components/README.zh-CN.md +90 -7
  166. package/src/components/RouterBridge.tsx +28 -4
  167. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  168. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  169. package/src/components/form/DialogFormLayout.tsx +5 -29
  170. package/src/components/form/VariableInput.tsx +101 -28
  171. package/src/components/form/__tests__/VariableInput.test.ts +85 -0
  172. package/src/components/form/filter/CollectionFilter.tsx +111 -0
  173. package/src/components/form/filter/CollectionFilterItem.tsx +184 -0
  174. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  175. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  176. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +247 -0
  177. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  178. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  179. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  180. package/src/components/form/filter/index.ts +13 -0
  181. package/src/components/form/filter/useFilterActionProps.ts +203 -0
  182. package/src/components/form/index.tsx +1 -0
  183. package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
  184. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  185. package/src/data-source/index.ts +10 -0
  186. package/src/flow/FlowPage.tsx +35 -7
  187. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  188. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  189. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  190. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  191. package/src/flow/actions/aclCheck.tsx +4 -0
  192. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  193. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  194. package/src/flow/actions/linkageRules.tsx +122 -0
  195. package/src/flow/actions/openView.tsx +28 -4
  196. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  197. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  198. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  199. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  200. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +35 -7
  201. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  202. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  203. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  204. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  205. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
  206. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  207. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  208. package/src/flow/admin-shell/admin-layout/index.ts +3 -0
  209. package/src/flow/admin-shell/admin-layout/useApplications.tsx +34 -1
  210. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  211. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  212. package/src/flow/components/AdminLayout.tsx +4 -154
  213. package/src/flow/components/FlowRoute.tsx +105 -15
  214. package/src/flow/components/filter/index.ts +3 -0
  215. package/src/flow/components/filter/useFilterOptions.ts +102 -0
  216. package/src/flow/index.ts +4 -0
  217. package/src/flow/models/actions/UpdateRecordActionModel.tsx +14 -95
  218. package/src/flow/models/actions/UpdateRecordActionUtils.ts +4 -7
  219. package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +26 -1
  220. package/src/flow/models/base/ActionModel.tsx +8 -1
  221. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  222. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  223. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  224. package/src/flow/models/base/RouteModel.tsx +1 -1
  225. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +63 -2
  226. package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +206 -0
  227. package/src/flow/models/blocks/assign-form/index.ts +1 -0
  228. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  229. package/src/flow/models/blocks/form/FormActionModel.tsx +30 -3
  230. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  231. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  232. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +71 -0
  233. package/src/flow/models/blocks/form/submitHandler.ts +8 -1
  234. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  235. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  236. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  237. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  238. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  239. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  240. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  241. package/src/flow/models/fields/CollectionSelectorFieldModel.tsx +8 -2
  242. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +8 -2
  243. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  245. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  246. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  247. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  248. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  249. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  250. package/src/index.ts +6 -0
  251. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  252. package/src/layout-manager/LayoutManager.tsx +185 -0
  253. package/src/layout-manager/LayoutRoute.tsx +138 -0
  254. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  255. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  256. package/src/layout-manager/index.ts +14 -0
  257. package/src/layout-manager/types.ts +22 -0
  258. package/src/layout-manager/utils.ts +37 -0
  259. package/src/nocobase-buildin-plugin/index.tsx +69 -67
  260. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  261. package/src/settings-center/index.ts +1 -1
  262. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  263. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  264. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  265. package/src/settings-center/plugin-manager/index.tsx +254 -0
  266. package/src/settings-center/plugin-manager/types.ts +35 -0
  267. package/src/settings-center/utils.tsx +8 -1
  268. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  269. package/src/theme/globalStyles.ts +10 -0
  270. package/src/utils/globalDeps.ts +2 -0
  271. package/es/collection-manager/interfaces/linkTo.d.ts +0 -90
  272. package/es/collection-manager/interfaces/o2o.d.ts +0 -621
  273. package/es/collection-manager/interfaces/properties/operators.d.ts +0 -294
  274. package/es/collection-manager/interfaces/subTable.d.ts +0 -172
  275. package/src/collection-manager/interfaces/linkTo.ts +0 -120
  276. package/src/collection-manager/interfaces/o2o.tsx +0 -561
  277. package/src/collection-manager/interfaces/subTable.ts +0 -218
  278. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -7,15 +7,18 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { useFlowEngine } from '@nocobase/flow-engine';
10
+ import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
11
11
  import React, { useEffect, useMemo, useRef, useState } from 'react';
12
12
  import { deviceType } from 'react-device-detect';
13
- import { useAdminLayoutRoutePage } from '../admin-shell/useAdminLayoutRoutePage';
14
13
  import { useParams } from 'react-router-dom';
15
14
  import { useApp } from '../../hooks/useApp';
16
15
  import { NocoBaseDesktopRouteType } from '../../flow-compat';
17
16
  import { resolveAdminRouteRuntimeTarget } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
17
+ import { getAdminLayoutModel, type AdminLayoutModel } from '../admin-shell/admin-layout/AdminLayoutModel';
18
+ import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutModel';
19
+ import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
18
20
  import { AppNotFound } from '../../components';
21
+ import { useKeepAlive } from '../../components/KeepAlive';
19
22
 
20
23
  type FlowRouteGuardState = {
21
24
  pending: boolean;
@@ -23,8 +26,66 @@ type FlowRouteGuardState = {
23
26
  notFound: boolean;
24
27
  };
25
28
 
26
- const BridgeFlowRoute = ({ pageUid }: { pageUid: string }) => {
29
+ export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
30
+
31
+ export type FlowRouteProps = {
32
+ pageUid?: string;
33
+ active?: boolean;
34
+ getLayoutModel?: (flowEngine: FlowEngine) => BaseLayoutModel | undefined;
35
+ legacyPageBehavior?: LegacyPageBehavior;
36
+ };
37
+
38
+ const getDefaultAdminLayoutModel = (flowEngine: FlowEngine) =>
39
+ getAdminLayoutModel<AdminLayoutModel>(flowEngine, { required: true });
40
+
41
+ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
42
+ const layout = contextLayout || flowEngine.context.layout;
43
+
44
+ if (layout?.uid) {
45
+ return getLayoutModel<BaseLayoutModel>(flowEngine, layout.uid, { required: true });
46
+ }
47
+
48
+ return getDefaultAdminLayoutModel(flowEngine);
49
+ };
50
+
51
+ const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: any): LegacyPageBehavior => {
52
+ const layout = contextLayout || flowEngine.context.layout;
53
+
54
+ if (layout?.routeName && layout.routeName !== 'admin') {
55
+ return 'notFound';
56
+ }
57
+
58
+ return 'redirect';
59
+ };
60
+
61
+ const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
62
+ if (flowEngine.getModel(pageUid)) {
63
+ return true;
64
+ }
65
+
66
+ const modelData = await flowEngine.modelRepository?.findOne({ uid: pageUid }).catch(() => null);
67
+ if (modelData?.uid) {
68
+ return true;
69
+ }
70
+
71
+ const model = await flowEngine.loadModel({ uid: pageUid }).catch(() => null);
72
+ if (model && flowEngine.getModel(pageUid) === model) {
73
+ flowEngine.removeModelWithSubModels(pageUid);
74
+ }
75
+ return !!model;
76
+ };
77
+
78
+ const BridgeFlowRoute = ({
79
+ pageUid,
80
+ active,
81
+ getLayoutModel,
82
+ }: {
83
+ pageUid: string;
84
+ active?: boolean;
85
+ getLayoutModel: (flowEngine: FlowEngine) => BaseLayoutModel | undefined;
86
+ }) => {
27
87
  const flowEngine = useFlowEngine();
88
+ const { active: keepAliveActive } = useKeepAlive();
28
89
  const routeRepository = flowEngine.context.routeRepository;
29
90
  const refreshDesktopRoutes = React.useMemo(
30
91
  () => routeRepository?.refreshAccessible.bind(routeRepository),
@@ -60,11 +121,13 @@ const BridgeFlowRoute = ({ pageUid }: { pageUid: string }) => {
60
121
  });
61
122
  }, [flowEngine]);
62
123
 
63
- useAdminLayoutRoutePage({
124
+ useLayoutRoutePage({
64
125
  flowEngine,
65
126
  pageUid,
127
+ active: active ?? keepAliveActive,
66
128
  refreshDesktopRoutes,
67
129
  layoutContentRef,
130
+ getLayoutModel,
68
131
  });
69
132
 
70
133
  return <div ref={layoutContentRef} />;
@@ -84,12 +147,20 @@ const BridgeFlowRoute = ({ pageUid }: { pageUid: string }) => {
84
147
  * @returns {JSX.Element} 当前动态页面的布局挂载节点
85
148
  * @throws {Error} 当缺少 `route.params.name` 时抛出异常
86
149
  */
87
- const FlowRoute = () => {
150
+ const FlowRoute = (props: FlowRouteProps = {}) => {
88
151
  const flowEngine = useFlowEngine();
152
+ const flowContext = useFlowContext<any>();
153
+ const contextLayout = flowContext?.layout;
154
+ const getLayoutModel = useMemo(
155
+ () => props.getLayoutModel || ((engine: FlowEngine) => getDefaultLayoutModel(engine, contextLayout)),
156
+ [contextLayout, props.getLayoutModel],
157
+ );
158
+ const legacyPageBehavior = props.legacyPageBehavior || getDefaultLegacyPageBehavior(flowEngine, contextLayout);
89
159
  const app = useApp();
90
160
  const routeRepository = flowEngine.context.routeRepository;
91
161
  const params = useParams();
92
- const pageUid = params?.name;
162
+ const pageUid = props.pageUid || params?.name;
163
+ const skipRouteRepositoryCheck = !routeRepository;
93
164
  const [guardState, setGuardState] = useState<FlowRouteGuardState>({
94
165
  pending: true,
95
166
  allowBridge: false,
@@ -99,7 +170,7 @@ const FlowRoute = () => {
99
170
  const requestIdRef = useRef(0);
100
171
 
101
172
  if (!pageUid) {
102
- throw new Error('[NocoBase] FlowRoute requires route.params.name.');
173
+ throw new Error('[NocoBase] FlowRoute requires pageUid or route.params.name.');
103
174
  }
104
175
 
105
176
  useEffect(() => {
@@ -109,7 +180,7 @@ const FlowRoute = () => {
109
180
  const run = async () => {
110
181
  setGuardState({ pending: true, allowBridge: false, notFound: false });
111
182
 
112
- if (!routeRepository?.isAccessibleLoaded?.()) {
183
+ if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
113
184
  try {
114
185
  await routeRepository?.ensureAccessibleLoaded?.();
115
186
  } catch (_error) {
@@ -124,8 +195,26 @@ const FlowRoute = () => {
124
195
  return;
125
196
  }
126
197
 
127
- const route = routeRepository?.getRouteBySchemaUid?.(pageUid);
198
+ const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
199
+ if (!route && legacyPageBehavior === 'notFound') {
200
+ const flowModelExists = await hasFlowModel(flowEngine, pageUid);
201
+ if (active && requestId === requestIdRef.current) {
202
+ setGuardState({ pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
203
+ }
204
+ return;
205
+ }
206
+
128
207
  if (route?.type === NocoBaseDesktopRouteType.page) {
208
+ if (legacyPageBehavior === 'notFound') {
209
+ setGuardState({ pending: false, allowBridge: false, notFound: true });
210
+ return;
211
+ }
212
+
213
+ if (legacyPageBehavior === 'bridge') {
214
+ setGuardState({ pending: false, allowBridge: true, notFound: false });
215
+ return;
216
+ }
217
+
129
218
  const target = resolveAdminRouteRuntimeTarget({
130
219
  app,
131
220
  route,
@@ -161,22 +250,23 @@ const FlowRoute = () => {
161
250
  return () => {
162
251
  active = false;
163
252
  };
164
- }, [app, pageUid, routeRepository]);
253
+ }, [app, flowEngine, legacyPageBehavior, pageUid, routeRepository, skipRouteRepositoryCheck]);
165
254
 
166
255
  const content = useMemo(() => {
167
256
  if (guardState.pending) {
168
257
  return null;
169
258
  }
170
259
 
260
+ if (guardState.notFound) {
261
+ return <AppNotFound />;
262
+ }
263
+
171
264
  if (!guardState.allowBridge) {
172
- if (guardState.notFound) {
173
- return <AppNotFound />;
174
- }
175
265
  return null;
176
266
  }
177
267
 
178
- return <BridgeFlowRoute pageUid={pageUid} />;
179
- }, [guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
268
+ return <BridgeFlowRoute pageUid={pageUid} active={props.active} getLayoutModel={getLayoutModel} />;
269
+ }, [getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid, props.active]);
180
270
 
181
271
  return content;
182
272
  };
@@ -15,3 +15,6 @@ export { VariableFilterItem } from './VariableFilterItem';
15
15
  export type { VariableFilterItemProps, VariableFilterItemValue } from './VariableFilterItem';
16
16
  export { LinkageFilterItem } from './LinkageFilterItem';
17
17
  export type { LinkageFilterItemProps, LinkageFilterItemValue } from './LinkageFilterItem';
18
+ export { useFilterOptions } from './useFilterOptions';
19
+ export type { FilterOption, UseFilterOptionsArgs } from './useFilterOptions';
20
+ // Higher-level filter compositions (`CollectionFilterItem`, `useFilterActionProps`, `createCollectionFilterItem`) live under `src/components/form/filter/`. They compose these flow primitives on top of a `Collection` binding — the dependency direction is form/filter → flow/components/filter, never the reverse.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import type { Collection } from '@nocobase/flow-engine';
11
+ import { useMemo } from 'react';
12
+ import { fieldsToOptions } from './fieldsToOptions';
13
+
14
+ /**
15
+ * One operator entry on a `FilterOption`. Mirrors v1's interface-defined operator shape so the v2 filter value renderer can pick up the same per-operator value-side schema (e.g. datetime operators wanting the smart date picker, array/enum operators wanting a tag-mode Select).
16
+ */
17
+ export type FilterOperator = {
18
+ value: string;
19
+ label: string;
20
+ /**
21
+ * Per-operator override for the value-side renderer. Wins over the field's own `uiSchema` when set. The `x-component` string is looked up against the v2 filter component registry.
22
+ */
23
+ schema?: { 'x-component'?: string; 'x-component-props'?: Record<string, any> } & Record<string, any>;
24
+ /** Operator takes no right-hand value (e.g. `$empty`, `$notEmpty`). */
25
+ noValue?: boolean;
26
+ [key: string]: any;
27
+ };
28
+
29
+ /** Single field-tree node returned by `useFilterOptions`. */
30
+ export type FilterOption = {
31
+ name: string;
32
+ type?: string;
33
+ target?: string;
34
+ title: string;
35
+ schema?: Record<string, any>;
36
+ operators?: FilterOperator[];
37
+ children?: FilterOption[];
38
+ };
39
+
40
+ export interface UseFilterOptionsArgs {
41
+ /**
42
+ * **Preferred way to restrict fields**, alongside `nonfilterableFieldNames`.
43
+ *
44
+ * Whitelist of root-level field names to expose. Empty/undefined means "all filterable fields". The whitelist applies only at depth 1 — nested fields under an allowed association field are always included, matching the legacy v1 `Filter.Action` behaviour.
45
+ */
46
+ filterableFieldNames?: string[];
47
+ /**
48
+ * **Preferred way to restrict fields**, alongside `filterableFieldNames`.
49
+ *
50
+ * Blacklist of root-level field names to drop. Mirrors v1's `nonfilterable: [...]` schema prop on `Filter.Action`. Applies at depth 1 only, same as the whitelist. When both `filterableFieldNames` and `nonfilterableFieldNames` are provided, both apply: the final field set is `(whitelist or all) minus blacklist`.
51
+ */
52
+ nonfilterableFieldNames?: string[];
53
+ /**
54
+ * Bypass the `filterableFieldNames` whitelist (mirrors v1 `noIgnore`).
55
+ *
56
+ * Legacy escape hatch from v1 schemas — prefer adjusting `filterableFieldNames` / `nonfilterableFieldNames` instead. Kept only for parity with existing v1 schemas that already set `noIgnore`.
57
+ */
58
+ noIgnore?: boolean;
59
+ /** Translator used for field/operator labels. Defaults to identity. */
60
+ t?: (key: string) => string;
61
+ }
62
+
63
+ const identity = (s: string) => s;
64
+
65
+ /**
66
+ * v2 equivalent of v1's `useFilterOptions`/`useFilterFieldOptions`. Walks a `Collection`'s fields and returns the nested option tree consumed by antd `Cascader` in `CollectionFilterItem` (and any other v2 filter surface that wants the same field picker).
67
+ *
68
+ * Mirrors v1 in two ways that matter:
69
+ * - association fields (belongsTo / hasMany / m2m / etc.) are kept and recursed into via `fieldsToOptions`'s `nested` branch — so picking `user.username` is a first-class action, just like the legacy cascader.
70
+ * - the whitelist applies at depth 1 only, so capping the root field list (e.g. to `['lockedTs', 'unlockTs', 'user']`) doesn't accidentally hide the nested `user.username` / `user.nickname` leaves.
71
+ */
72
+ export function useFilterOptions(collection: Collection | undefined, args: UseFilterOptionsArgs = {}): FilterOption[] {
73
+ const { filterableFieldNames, nonfilterableFieldNames, noIgnore = false, t = identity } = args;
74
+
75
+ const fields = useMemo(() => collection?.getFields() || [], [collection]);
76
+
77
+ const ignoreFieldsNames = useMemo(() => {
78
+ // Whitelist contribution: every field not in `filterableFieldNames`.
79
+ // Skipped when `noIgnore` is set, or when no whitelist was provided.
80
+ const whitelistIgnored =
81
+ noIgnore || !filterableFieldNames?.length
82
+ ? []
83
+ : fields.map((f) => f.name).filter((n) => !filterableFieldNames.includes(n));
84
+ // Blacklist contribution: explicit names. Always applied, even with
85
+ // `noIgnore` (the blacklist's whole job is to subtract specific fields).
86
+ const blacklistIgnored = nonfilterableFieldNames ?? [];
87
+ if (!blacklistIgnored.length) return whitelistIgnored;
88
+ // Union the two so the final ignore set is `whitelist-derived ∪ blacklist`.
89
+ return Array.from(new Set([...whitelistIgnored, ...blacklistIgnored]));
90
+ }, [fields, filterableFieldNames, nonfilterableFieldNames, noIgnore]);
91
+
92
+ return useMemo(
93
+ () =>
94
+ fieldsToOptions(
95
+ fields.filter((field) => field.target !== 'attachments' && field.interface !== 'formula'),
96
+ 1,
97
+ ignoreFieldsNames,
98
+ t,
99
+ ).filter(Boolean) as FilterOption[],
100
+ [fields, ignoreFieldsNames, t],
101
+ );
102
+ }
package/src/flow/index.ts CHANGED
@@ -100,7 +100,11 @@ export * from './utils';
100
100
  export * from './actions';
101
101
  export * from './system-settings';
102
102
  export * from './admin-shell/admin-layout';
103
+ export * from './admin-shell/BaseLayoutModel';
104
+ export * from './admin-shell/BaseLayoutRouteCoordinator';
103
105
  export * from './admin-shell/AdminLayoutRouteCoordinator';
106
+ export * from './admin-shell/useLayoutRoutePage';
107
+ export * from './admin-shell/useAdminLayoutRoutePage';
104
108
  export * from '../settings-center';
105
109
  export { openViewFlow } from './flows/openViewFlow';
106
110
  export { editMarkdownFlow } from './flows/editMarkdownFlow';
@@ -7,75 +7,22 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { tExpr, FlowModelRenderer, useFlowEngine, useFlowSettingsContext } from '@nocobase/flow-engine';
10
+ import { tExpr, useFlowSettingsContext } from '@nocobase/flow-engine';
11
11
  import { Alert, ButtonProps } from 'antd';
12
- import React, { useEffect, useRef } from 'react';
12
+ import React from 'react';
13
13
  import { AxiosRequestConfig } from 'axios';
14
14
  import { ActionModel, ActionSceneEnum } from '../base/ActionModel';
15
- import { CollectionActionModel } from '../base/CollectionActionModel';
16
- import { RecordActionModel } from '../base/RecordActionModel';
17
15
  import { AssignFormModel } from '../blocks/assign-form/AssignFormModel';
16
+ import {
17
+ createAssignFieldValuesStep,
18
+ createAssignFormSubModelOptions,
19
+ getAssignFieldValuesDefaultParams,
20
+ } from '../blocks/assign-form/assignFieldValuesFlow';
18
21
  import { applyUpdateRecordAction } from './UpdateRecordActionUtils';
19
22
  // import { RemoteFlowModelRenderer } from '../../FlowPage';
20
23
 
21
24
  const SETTINGS_FLOW_KEY = 'assignSettings';
22
25
 
23
- // 配置态编辑器:渲染 assignForm 子模型
24
- function AssignFieldsEditor() {
25
- const { model, blockModel } = useFlowSettingsContext();
26
- const action: any = model;
27
- const engine = useFlowEngine();
28
- const initializedRef = useRef(false);
29
- const [formModel, setFormModel] = React.useState<AssignFormModel | null>(null);
30
-
31
- // 初始化回填
32
- useEffect(() => {
33
- let cancelled = false;
34
- (async () => {
35
- const loaded = (await engine.loadOrCreateModel(
36
- {
37
- parentId: action.uid,
38
- subKey: 'assignForm',
39
- use: 'AssignFormModel',
40
- },
41
- { skipSave: !model.context.flowSettingsEnabled },
42
- )) as AssignFormModel;
43
- if (cancelled) return;
44
- setFormModel(loaded);
45
- action.assignFormUid = (loaded as any)?.uid || action.assignFormUid;
46
- })();
47
- return () => {
48
- cancelled = true;
49
- };
50
- }, [action, engine]);
51
-
52
- // 初始化回填(在子模型加载完成后)
53
- useEffect(() => {
54
- if (initializedRef.current) return;
55
- if (!formModel) return;
56
- const prev = action.getStepParams?.(SETTINGS_FLOW_KEY, 'assignFieldValues') || {};
57
- // 注入资源上下文:与所在表格区块一致
58
- const coll = blockModel?.collection || action?.context?.collection;
59
- const dsKey = coll?.dataSourceKey;
60
- const collName = coll?.name;
61
- if (dsKey && collName) {
62
- formModel.setStepParams('resourceSettings', 'init', {
63
- dataSourceKey: dsKey,
64
- collectionName: collName,
65
- });
66
- }
67
- formModel.setInitialAssignedValues(prev?.assignedValues || {});
68
- // 批量配置态:移除 ctx.record(Action 为区块级,不具备单条记录上下文)
69
- const isBulk = action instanceof CollectionActionModel && !(action instanceof RecordActionModel);
70
- if (isBulk && (formModel as any)?.context?.defineProperty) {
71
- formModel.context.defineProperty('record', { get: () => undefined, cache: false });
72
- }
73
- initializedRef.current = true;
74
- }, [action, blockModel?.collection, formModel]);
75
-
76
- return formModel ? <FlowModelRenderer model={formModel} showFlowSettings={false} /> : null;
77
- }
78
-
79
26
  function Info() {
80
27
  const ctx = useFlowSettingsContext();
81
28
  return (
@@ -124,16 +71,9 @@ UpdateRecordActionModel.define({
124
71
  sort: 50,
125
72
  // 使用函数型 createModelOptions,从父级上下文提取资源信息,直接注入到子模型的 resourceSettings.init
126
73
  createModelOptions: (ctx) => {
127
- const dsKey = ctx.collection.dataSourceKey;
128
- const collName = ctx.collection.name;
129
- const init = dsKey && collName ? { dataSourceKey: dsKey, collectionName: collName } : undefined;
130
74
  return {
131
75
  subModels: {
132
- assignForm: {
133
- async: true,
134
- use: 'AssignFormModel',
135
- stepParams: { resourceSettings: { init } },
136
- },
76
+ assignForm: createAssignFormSubModelOptions(ctx),
137
77
  },
138
78
  };
139
79
  },
@@ -155,32 +95,12 @@ UpdateRecordActionModel.registerFlow({
155
95
  content: tExpr('Are you sure you want to perform the Update record action?'),
156
96
  },
157
97
  },
158
- assignFieldValues: {
98
+ assignFieldValues: createAssignFieldValuesStep({
99
+ settingsFlowKey: SETTINGS_FLOW_KEY,
159
100
  title: tExpr('Field values'),
160
- uiSchema() {
161
- return {
162
- tip: {
163
- 'x-decorator': 'FormItem',
164
- 'x-component': () => <Info />,
165
- },
166
- editor: {
167
- 'x-decorator': 'FormItem',
168
- 'x-component': () => <AssignFieldsEditor />,
169
- },
170
- };
171
- },
172
- async beforeParamsSave(ctx) {
173
- const m = ctx.model as UpdateRecordActionModel;
174
- // 跨视图栈按 uid 定位到设置面板中的真实 AssignForm 实例
175
- const form: AssignFormModel = m?.assignFormUid && ctx.engine.getModel?.(m.assignFormUid, true);
176
- if (!form) return;
177
- await form?.form?.validateFields?.();
178
- const assignedValues = form?.getAssignedValues?.() || {};
179
- const grid = form?.subModels?.grid;
180
- const items = grid?.subModels?.items || [];
181
- ctx.model.setStepParams(SETTINGS_FLOW_KEY, 'assignFieldValues', { assignedValues });
182
- },
183
- },
101
+ tipComponent: Info,
102
+ validateBeforeSave: true,
103
+ }),
184
104
  },
185
105
  });
186
106
 
@@ -190,8 +110,7 @@ UpdateRecordActionModel.registerFlow({
190
110
  steps: {
191
111
  apply: {
192
112
  async defaultParams(ctx) {
193
- const step = ctx.model.getStepParams(SETTINGS_FLOW_KEY, 'assignFieldValues') || {};
194
- return { assignedValues: step?.assignedValues || {} };
113
+ return getAssignFieldValuesDefaultParams(ctx, SETTINGS_FLOW_KEY);
195
114
  },
196
115
  async handler(ctx, params) {
197
116
  await applyUpdateRecordAction(ctx, params, { settingsFlowKey: SETTINGS_FLOW_KEY });
@@ -7,8 +7,9 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { MultiRecordResource, SingleRecordResource, resolveRunJSObjectValues } from '@nocobase/flow-engine';
10
+ import { MultiRecordResource, SingleRecordResource } from '@nocobase/flow-engine';
11
11
  import { dispatchEventDeep } from '../../utils';
12
+ import { resolveAssignFieldValues } from '../blocks/assign-form/assignFieldValuesFlow';
12
13
 
13
14
  export async function refreshLinkageRulesAfterUpdate(ctx: any) {
14
15
  const blockModel = ctx?.blockModel || ctx?.model?.context?.blockModel || ctx?.model;
@@ -59,12 +60,8 @@ export async function applyUpdateRecordAction(
59
60
  const confirmParams = savedConfirm && typeof savedConfirm === 'object' ? savedConfirm : { enable: false };
60
61
  await ctx.runAction('confirm', confirmParams);
61
62
 
62
- let assignedValues: Record<string, any> = {};
63
- try {
64
- assignedValues = await resolveRunJSObjectValues(ctx, params?.assignedValues);
65
- } catch (error) {
66
- console.error('[UpdateRecordAction] RunJS execution failed', error);
67
- ctx.message.error(ctx.t('RunJS execution failed'));
63
+ const assignedValues = await resolveAssignFieldValues(ctx, params?.assignedValues, 'UpdateRecordAction');
64
+ if (!assignedValues) {
68
65
  return;
69
66
  }
70
67
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { describe, it, expect } from 'vitest';
11
11
  import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
12
- import { UpdateRecordActionModel } from '../../..'; // 这样可以解决循环依赖问题
12
+ import { FormSubmitActionModel, UpdateRecordActionModel } from '../../..'; // 这样可以解决循环依赖问题
13
13
 
14
14
  /**
15
15
  * 精简版 AssignFormModel(仅用于单测):
@@ -52,4 +52,29 @@ describe('AssignForm value refill and save (beforeParamsSave)', () => {
52
52
  const saved = action.getStepParams('assignSettings', 'assignFieldValues');
53
53
  expect(saved?.assignedValues).toEqual({ nickname: 'Alice', score: 99 });
54
54
  });
55
+
56
+ it('FormSubmitActionModel: reuses assignFieldValues step and saves assignedValues from AssignForm', async () => {
57
+ const root = new FlowEngine();
58
+
59
+ root.registerModels({ FormSubmitActionModel, AssignFormModel: TestAssignFormModel });
60
+
61
+ const action = root.createModel<FormSubmitActionModel>({ use: 'FormSubmitActionModel', uid: 'submit-u' });
62
+ const form = root.createModel<TestAssignFormModel>({
63
+ use: 'AssignFormModel',
64
+ uid: 'submit-form-u',
65
+ parentId: action.uid,
66
+ subKey: 'assignForm',
67
+ });
68
+ form.setAssignedValues({ status: 'published' });
69
+ action.assignFormUid = form.uid;
70
+
71
+ const flow = action.getFlow('submitSettings') as any;
72
+ const step = flow?.steps?.assignFieldValues;
73
+ expect(step?.beforeParamsSave).toBeTypeOf('function');
74
+
75
+ await step.beforeParamsSave({ engine: root, model: action });
76
+
77
+ const saved = action.getStepParams('submitSettings', 'assignFieldValues');
78
+ expect(saved?.assignedValues).toEqual({ status: 'published' });
79
+ });
55
80
  });
@@ -80,7 +80,14 @@ ActionModel.registerFlow({
80
80
  };
81
81
  },
82
82
  defaultParams(ctx) {
83
- return ctx.model.defaultProps;
83
+ const defaultProps = ctx.model.defaultProps || {};
84
+ if (!ctx.model.enableEditColor) {
85
+ return defaultProps;
86
+ }
87
+ return {
88
+ ...defaultProps,
89
+ color: ctx.model.props?.color ?? defaultProps.color ?? ctx.themeToken?.colorPrimary,
90
+ };
84
91
  },
85
92
  handler(ctx, params) {
86
93
  const { title, tooltip, ...rest } = params;
@@ -40,6 +40,15 @@ type PageModelStructure = {
40
40
  };
41
41
  };
42
42
 
43
+ type CurrentRouteWithTabs = {
44
+ id?: string | number | null;
45
+ enableTabs?: boolean;
46
+ };
47
+
48
+ type PageModelContextWithRoute = {
49
+ currentRoute?: CurrentRouteWithTabs | null;
50
+ };
51
+
43
52
  export class PageModel extends FlowModel<PageModelStructure> {
44
53
  tabBarExtraContent: { left?: ReactNode; right?: ReactNode } = {};
45
54
  private viewActivatedListener?: (_payload?: unknown) => void;
@@ -53,9 +62,15 @@ export class PageModel extends FlowModel<PageModelStructure> {
53
62
  * 根页面标签页开关以路由表为准,避免 flow model 里的旧配置覆盖路由管理设置。
54
63
  */
55
64
  private getEnableTabs(): boolean {
56
- const routeEnableTabs = (this.context as any)?.currentRoute?.enableTabs;
57
- if (this.props.routeId != null && typeof routeEnableTabs === 'boolean') {
58
- return routeEnableTabs;
65
+ const currentRoute = (this.context as PageModelContextWithRoute).currentRoute;
66
+ const routeId = this.props.routeId;
67
+ if (
68
+ routeId != null &&
69
+ currentRoute?.id != null &&
70
+ String(currentRoute.id) === String(routeId) &&
71
+ typeof currentRoute.enableTabs === 'boolean'
72
+ ) {
73
+ return currentRoute.enableTabs;
59
74
  }
60
75
  return !!this.props.enableTabs;
61
76
  }
@@ -77,16 +92,27 @@ export class PageModel extends FlowModel<PageModelStructure> {
77
92
  if (this.unmounted) return;
78
93
  // Only skip when explicitly inactive; treat "unknown" (undefined) as active for backward compatibility.
79
94
  if (getPageActive(this.context) === false) return;
80
- const activeKey = this.getActiveTabKey();
81
- if (activeKey) {
82
- this.invokeTabModelLifecycleMethod(activeKey, 'onActive', forceRefresh);
83
- }
95
+ this.activateCurrentTab(forceRefresh);
84
96
  })
85
97
  .catch(() => {
86
98
  // ignore
87
99
  });
88
100
  }
89
101
 
102
+ activateCurrentTab(forceRefresh = false) {
103
+ const activeKey = this.getActiveTabKey();
104
+ if (activeKey) {
105
+ this.invokeTabModelLifecycleMethod(activeKey, 'onActive', forceRefresh);
106
+ }
107
+ }
108
+
109
+ deactivateCurrentTab() {
110
+ const activeKey = this.props.tabActiveKey || this.getFirstTab()?.uid;
111
+ if (activeKey) {
112
+ this.invokeTabModelLifecycleMethod(activeKey, 'onInactive');
113
+ }
114
+ }
115
+
90
116
  onMount(): void {
91
117
  super.onMount();
92
118
  this.unmounted = false;
@@ -98,10 +124,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
98
124
  // We align this with the existing tab lifecycle by invoking `onActive` for the current tab blocks.
99
125
  if (!this.viewActivatedListener) {
100
126
  this.viewActivatedListener = (_payload?: unknown) => {
101
- const activeKey = this.getActiveTabKey();
102
- if (activeKey) {
103
- this.invokeTabModelLifecycleMethod(activeKey, 'onActive');
104
- }
127
+ this.activateCurrentTab();
105
128
  };
106
129
  this.flowEngine?.emitter?.on?.(VIEW_ACTIVATED_EVENT, this.viewActivatedListener);
107
130
  }
@@ -113,10 +136,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
113
136
  emitterActivatedVersion > 0 && emitterActivatedVersion !== this.lastSeenEmitterViewActivatedVersion;
114
137
  this.lastSeenEmitterViewActivatedVersion = emitterActivatedVersion;
115
138
  if (shouldCatchUp && getPageActive(this.context) !== false) {
116
- const activeKey = this.getActiveTabKey();
117
- if (activeKey) {
118
- this.invokeTabModelLifecycleMethod(activeKey, 'onActive');
119
- }
139
+ this.activateCurrentTab();
120
140
  }
121
141
 
122
142
  // When data is written within the same view, trigger an "active" lifecycle pass so blocks can refresh based on dirty.
@@ -141,11 +161,21 @@ export class PageModel extends FlowModel<PageModelStructure> {
141
161
  super.onUnmount();
142
162
  }
143
163
 
144
- invokeTabModelLifecycleMethod(tabActiveKey: string, method: 'onActive' | 'onInactive', forceRefresh = false) {
164
+ invokeTabModelLifecycleMethod(
165
+ tabActiveKey: string | undefined,
166
+ method: 'onActive' | 'onInactive',
167
+ forceRefresh = false,
168
+ ) {
169
+ if (!tabActiveKey) {
170
+ return;
171
+ }
172
+
145
173
  if (method === 'onActive' && this.context?.pageInfo) {
146
174
  this.context.pageInfo.version = 'v2';
147
175
  }
148
- const tabModel = this.flowEngine.getModel(tabActiveKey) as BasePageTabModel | undefined;
176
+ const tabModel =
177
+ this.findSubModel('tabs', (model) => model.uid === tabActiveKey) ||
178
+ (this.flowEngine.getModel(tabActiveKey) as BasePageTabModel | undefined);
149
179
 
150
180
  if (tabModel) {
151
181
  if (tabModel.context.tabActive) {
@@ -183,7 +213,10 @@ export class PageModel extends FlowModel<PageModelStructure> {
183
213
  const routePathname = this.flowEngine?.context?.route?.pathname;
184
214
  // In route-managed multi-view mode, only the top view in URL should mutate document.title.
185
215
  if (hasRouteNavigation && currentViewUid && typeof routePathname === 'string') {
186
- const topViewUid = parsePathnameToViewParams(routePathname).at(-1)?.viewUid;
216
+ const layoutRoutePath = this.context?.layout?.routePath;
217
+ const topViewUid = parsePathnameToViewParams(routePathname, {
218
+ basePath: this.context?.layoutRoute?.basePathname || (layoutRoutePath?.startsWith('/') ? layoutRoutePath : ''),
219
+ }).at(-1)?.viewUid;
187
220
  if (topViewUid && topViewUid !== currentViewUid) {
188
221
  return;
189
222
  }