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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouteRepository.d.ts +8 -0
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +1 -0
  18. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  19. package/es/entry-actions/index.d.ts +9 -0
  20. package/es/flow/actions/index.d.ts +1 -1
  21. package/es/flow/actions/linkageRules.d.ts +27 -0
  22. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  23. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  25. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  26. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  27. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  28. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  29. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  30. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  31. package/es/flow/components/FlowRoute.d.ts +2 -2
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  34. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  35. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  36. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  37. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  38. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  39. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  40. package/es/flow/index.d.ts +1 -0
  41. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  42. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  43. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  44. package/es/flow/models/base/ActionModelCore.d.ts +6 -2
  45. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  46. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  47. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  48. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  49. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  50. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  51. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  52. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  53. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  54. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  55. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  56. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  57. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  58. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  59. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  60. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  61. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  62. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  63. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  64. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  65. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  66. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  67. package/es/flow-compat/passwordUtils.d.ts +1 -1
  68. package/es/flow-compat/routeTypes.d.ts +1 -0
  69. package/es/index.d.ts +7 -0
  70. package/es/index.mjs +301 -177
  71. package/es/ui-operation/index.d.ts +15 -0
  72. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  73. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  74. package/es/utils/index.d.ts +1 -0
  75. package/es/utils/markdownSanitize.d.ts +11 -0
  76. package/lib/index.js +313 -189
  77. package/lib/locale/languageCodes.js +3 -1
  78. package/package.json +9 -7
  79. package/src/APIClient.ts +90 -7
  80. package/src/Application.tsx +7 -1
  81. package/src/BaseApplication.tsx +13 -6
  82. package/src/RouteRepository.ts +25 -0
  83. package/src/RouterManager.tsx +142 -1
  84. package/src/__tests__/APIClient.test.tsx +58 -0
  85. package/src/__tests__/RouteRepository.test.ts +23 -0
  86. package/src/__tests__/RouterManager.test.ts +125 -0
  87. package/src/__tests__/app.test.tsx +73 -0
  88. package/src/__tests__/authRedirect.test.ts +17 -0
  89. package/src/__tests__/browserChecker.test.ts +61 -0
  90. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  91. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  92. package/src/__tests__/settings-center.test.tsx +270 -6
  93. package/src/acl/ACLProvider.tsx +2 -0
  94. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  95. package/src/authRedirect.ts +38 -0
  96. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  97. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  98. package/src/collection-manager/field-validation.ts +1 -1
  99. package/src/components/README.md +7 -1
  100. package/src/components/README.zh-CN.md +6 -1
  101. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  102. package/src/components/category-tabs/index.ts +10 -0
  103. package/src/components/form/JsonTextArea.tsx +4 -0
  104. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  105. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  106. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  107. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  108. package/src/components/form/TypedVariableInput.tsx +452 -101
  109. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  110. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  111. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  112. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  113. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  114. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  115. package/src/components/form/filter/index.ts +2 -0
  116. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  117. package/src/components/form/table/Table.tsx +81 -6
  118. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  119. package/src/components/index.ts +1 -0
  120. package/src/entry-actions/EntryActionManager.ts +76 -0
  121. package/src/entry-actions/index.ts +10 -0
  122. package/src/flow/FlowPage.tsx +38 -3
  123. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  124. package/src/flow/__tests__/FlowRoute.test.tsx +675 -12
  125. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  126. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  127. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  128. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  129. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  130. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  131. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  132. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  133. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  134. package/src/flow/actions/dataScopeFilter.ts +10 -1
  135. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  136. package/src/flow/actions/index.ts +2 -0
  137. package/src/flow/actions/linkageRules.tsx +144 -26
  138. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  139. package/src/flow/actions/openView.tsx +21 -1
  140. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  141. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  142. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  143. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
  144. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  145. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  146. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  147. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  148. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  149. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  150. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  151. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  152. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  153. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  154. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  155. package/src/flow/common/Markdown/Display.tsx +14 -3
  156. package/src/flow/common/Markdown/Edit.tsx +19 -7
  157. package/src/flow/common/Markdown/style.ts +4 -0
  158. package/src/flow/components/BlockItemCard.tsx +2 -2
  159. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  160. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  161. package/src/flow/components/FlowRoute.tsx +204 -46
  162. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  163. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  164. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  165. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  166. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  167. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  168. package/src/flow/components/field-value-variable/index.ts +12 -0
  169. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  170. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  171. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  172. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  173. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  174. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  175. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  176. package/src/flow/index.ts +3 -0
  177. package/src/flow/internal/components/Markdown/util.ts +2 -1
  178. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  179. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  180. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  181. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  182. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  183. package/src/flow/models/base/ActionModel.tsx +10 -8
  184. package/src/flow/models/base/ActionModelCore.tsx +16 -2
  185. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  186. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  187. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  188. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  189. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  190. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  191. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  192. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  193. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  194. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  195. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  196. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  197. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  198. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  199. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  200. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  201. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  202. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  203. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  204. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  205. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  206. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  207. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  208. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  209. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  210. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  211. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  212. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  213. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  214. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  215. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  216. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  217. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  218. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  219. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  220. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  221. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  222. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  223. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  224. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  225. package/src/flow/models/blocks/table/TableColumnModel.tsx +54 -11
  226. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  227. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  228. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  229. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +84 -0
  230. package/src/flow/models/blocks/table/utils.ts +7 -0
  231. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  232. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  233. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  234. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  235. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  236. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +54 -149
  237. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  238. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  239. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  240. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  241. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  242. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  243. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  245. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  246. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  247. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  248. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  249. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  250. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  251. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  252. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  253. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  254. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  255. package/src/flow/routeTransientInputArgs.ts +27 -0
  256. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  257. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  258. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  259. package/src/flow-compat/routeTypes.ts +1 -0
  260. package/src/index.ts +7 -0
  261. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  262. package/src/layout-manager/LayoutRoute.tsx +2 -1
  263. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  264. package/src/locale/languageCodes.ts +3 -1
  265. package/src/nocobase-buildin-plugin/index.tsx +19 -1
  266. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  267. package/src/ui-operation/index.ts +33 -0
  268. package/src/ui-operation/ui-operation-codec.ts +54 -0
  269. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  270. package/src/utils/index.tsx +1 -0
  271. package/src/utils/markdownSanitize.ts +88 -0
@@ -22,7 +22,6 @@ import {
22
22
  createRecordResolveOnServerWithLocal,
23
23
  } from '@nocobase/flow-engine';
24
24
  import { Button, Form, Skeleton, Space } from 'antd';
25
- import _ from 'lodash';
26
25
  import React from 'react';
27
26
  import { FieldModel } from '../../base/FieldModel';
28
27
  import { FormComponent } from './FormBlockModel';
@@ -31,6 +30,37 @@ import { FormItemModel } from './FormItemModel';
31
30
  export const QUICK_EDIT_POPOVER_MAX_HEIGHT = 'calc(100vh - 96px)';
32
31
  export const QUICK_EDIT_FORM_MAX_HEIGHT = 'calc(100vh - 160px)';
33
32
  export const QUICK_EDIT_MARKDOWN_HEIGHT = 'min(480px, calc(100vh - 320px))';
33
+ const QUICK_EDIT_MOBILE_DRAWER_HEIGHT = '50vh';
34
+ const QUICK_EDIT_MOBILE_FORM_MAX_HEIGHT = 'calc(50vh - var(--nb-mobile-page-header-height, 40px) - 132px)';
35
+ const QUICK_EDIT_MOBILE_CONTENT_PADDING = '8px var(--nb-mobile-page-tabs-content-padding, 12px) 0';
36
+ const QUICK_EDIT_MOBILE_ACTIONS_PADDING =
37
+ '8px var(--nb-mobile-page-tabs-content-padding, 12px) calc(80px + env(safe-area-inset-bottom, 0px))';
38
+ const QUICK_EDIT_MOBILE_MEDIA_QUERY = '(max-width: 768px)';
39
+
40
+ type QuickEditViewBeforeClosePayload = {
41
+ result?: unknown;
42
+ force?: boolean;
43
+ [key: string]: unknown;
44
+ };
45
+
46
+ type QuickEditViewBeforeCloseHandler = (
47
+ payload: QuickEditViewBeforeClosePayload,
48
+ ) => Promise<boolean | void> | boolean | void;
49
+
50
+ type QuickEditViewUpdateConfig = {
51
+ preventClose?: boolean;
52
+ [key: string]: unknown;
53
+ };
54
+
55
+ type QuickEditViewContainer = {
56
+ close: (result?: unknown, force?: boolean) => Promise<boolean | void> | boolean | void;
57
+ update?: (newConfig: QuickEditViewUpdateConfig) => unknown;
58
+ beforeClose?: QuickEditViewBeforeCloseHandler;
59
+ };
60
+
61
+ type QuickEditViewContext = {
62
+ defineProperty?: (key: string, options: { value: unknown }) => void;
63
+ };
34
64
 
35
65
  export function getQuickEditFieldProps(collectionField: CollectionField, fieldProps?: Record<string, any>) {
36
66
  const nextProps = { ...collectionField.getComponentProps(), ...(fieldProps || {}) };
@@ -40,15 +70,92 @@ export function getQuickEditFieldProps(collectionField: CollectionField, fieldPr
40
70
  return nextProps;
41
71
  }
42
72
 
73
+ function getQuickEditMobileLayoutState(flowEngine: FlowEngine, sourceFieldModel?: FlowModel) {
74
+ const sourceMobileLayout = sourceFieldModel?.context?.isMobileLayout;
75
+ if (typeof sourceMobileLayout === 'boolean') {
76
+ return { isMobileLayout: sourceMobileLayout, inheritsMobileContext: sourceMobileLayout };
77
+ }
78
+
79
+ const engineMobileLayout = flowEngine.context?.isMobileLayout;
80
+ if (typeof engineMobileLayout === 'boolean') {
81
+ return { isMobileLayout: engineMobileLayout, inheritsMobileContext: engineMobileLayout };
82
+ }
83
+
84
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
85
+ return { isMobileLayout: false, inheritsMobileContext: false };
86
+ }
87
+
88
+ return {
89
+ isMobileLayout: window.matchMedia(QUICK_EDIT_MOBILE_MEDIA_QUERY).matches,
90
+ inheritsMobileContext: false,
91
+ };
92
+ }
93
+
94
+ function getQuickEditTitle(
95
+ flowEngine: FlowEngine,
96
+ dataSourceKey: string,
97
+ collectionName: string,
98
+ fieldPath: string,
99
+ sourceFieldModel?: FlowModel,
100
+ fieldProps?: Record<string, unknown>,
101
+ ): React.ReactNode {
102
+ const sourceColumnTitle = sourceFieldModel?.parent?.props?.title;
103
+ if (sourceColumnTitle) {
104
+ return sourceColumnTitle;
105
+ }
106
+
107
+ const fieldPropsTitle = fieldProps?.title;
108
+ if (fieldPropsTitle) {
109
+ return fieldPropsTitle as React.ReactNode;
110
+ }
111
+
112
+ const collectionField = flowEngine.context.dataSourceManager.getCollectionField(
113
+ `${dataSourceKey}.${collectionName}.${fieldPath}`,
114
+ ) as CollectionField | undefined;
115
+ return collectionField?.title || fieldPath;
116
+ }
117
+
118
+ function createQuickEditViewContainer(view: QuickEditViewContainer): QuickEditViewContainer {
119
+ let preventClose = false;
120
+ let nextBeforeClose = view.beforeClose;
121
+ const beforeClose: QuickEditViewBeforeCloseHandler = async (payload) => {
122
+ if (preventClose && !payload?.force) {
123
+ return false;
124
+ }
125
+
126
+ const result = await nextBeforeClose?.(payload);
127
+ return result !== false;
128
+ };
129
+ view.beforeClose = beforeClose;
130
+
131
+ return {
132
+ ...view,
133
+ close(result, force) {
134
+ return view.close(result, force);
135
+ },
136
+ update(newConfig) {
137
+ if (Object.prototype.hasOwnProperty.call(newConfig, 'preventClose')) {
138
+ preventClose = !!newConfig.preventClose;
139
+ }
140
+ return view.update?.(newConfig);
141
+ },
142
+ get beforeClose() {
143
+ return view.beforeClose;
144
+ },
145
+ set beforeClose(value) {
146
+ nextBeforeClose = value;
147
+ view.beforeClose = beforeClose;
148
+ },
149
+ };
150
+ }
151
+
43
152
  export class QuickEditFormModel extends FlowModel {
44
153
  fieldPath: string;
45
154
 
46
155
  declare resource: SingleRecordResource;
47
156
  declare collection: Collection;
48
157
 
49
- now: number = Date.now();
50
-
51
- viewContainer: any;
158
+ declare viewContainer: QuickEditViewContainer;
52
159
  __onSubmitSuccess;
53
160
  _fieldProps: any;
54
161
  _onOk: any;
@@ -89,21 +196,75 @@ export class QuickEditFormModel extends FlowModel {
89
196
  onOk,
90
197
  sourceFieldModelUid,
91
198
  } = options;
92
- const model = flowEngine.createModel({
93
- use: 'QuickEditFormModel',
94
- stepParams: {
95
- quickEditFormSettings: {
96
- init: {
97
- dataSourceKey,
98
- collectionName,
99
- fieldPath,
199
+ const sourceFieldModel = sourceFieldModelUid ? flowEngine.getModel<FlowModel>(sourceFieldModelUid) : undefined;
200
+ const model = flowEngine.createModel(
201
+ {
202
+ use: 'QuickEditFormModel',
203
+ stepParams: {
204
+ quickEditFormSettings: {
205
+ init: {
206
+ dataSourceKey,
207
+ collectionName,
208
+ fieldPath,
209
+ },
100
210
  },
101
211
  },
102
212
  },
103
- }) as QuickEditFormModel;
213
+ sourceFieldModel ? { delegate: sourceFieldModel.context } : undefined,
214
+ ) as QuickEditFormModel;
104
215
 
105
- console.log('QuickEditFormModel.open2', Date.now() - model.now);
106
- model.now = Date.now();
216
+ const bodyStyles = {
217
+ maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
218
+ overflowY: 'auto',
219
+ overscrollBehavior: 'contain',
220
+ };
221
+ const mobileBodyStyles = {
222
+ ...bodyStyles,
223
+ maxHeight: QUICK_EDIT_MOBILE_DRAWER_HEIGHT,
224
+ };
225
+ const content = (view: QuickEditViewContainer, viewContext?: QuickEditViewContext) => {
226
+ if (mobileLayoutState.isMobileLayout) {
227
+ viewContext?.defineProperty?.('isMobileLayout', { value: true });
228
+ }
229
+ model.viewContainer = createQuickEditViewContainer(view);
230
+ model.__onSubmitSuccess = onSuccess;
231
+ model._fieldProps = fieldProps;
232
+ model._onOk = onOk;
233
+ return (
234
+ <FlowModelRenderer
235
+ fallback={<Skeleton.Input size="small" />}
236
+ model={model}
237
+ inputArgs={{ filterByTk, record, sourceFieldModelUid }}
238
+ />
239
+ );
240
+ };
241
+ const mobileLayoutState = getQuickEditMobileLayoutState(flowEngine, sourceFieldModel);
242
+ if (mobileLayoutState.isMobileLayout) {
243
+ model.context.defineProperty('isMobileLayout', { value: true });
244
+ const viewer = sourceFieldModel?.context?.viewer || flowEngine.context.viewer;
245
+ const title = getQuickEditTitle(
246
+ flowEngine,
247
+ dataSourceKey,
248
+ collectionName,
249
+ fieldPath,
250
+ sourceFieldModel,
251
+ fieldProps,
252
+ );
253
+ await viewer.open({
254
+ type: 'drawer',
255
+ title,
256
+ closable: true,
257
+ placement: 'bottom',
258
+ inputArgs: {
259
+ isMobileLayout: true,
260
+ },
261
+ styles: {
262
+ body: mobileBodyStyles,
263
+ },
264
+ content,
265
+ });
266
+ return;
267
+ }
107
268
 
108
269
  await flowEngine.context.viewer.open({
109
270
  type: 'popover',
@@ -112,25 +273,10 @@ export class QuickEditFormModel extends FlowModel {
112
273
  styles: {
113
274
  body: {
114
275
  width: 420,
115
- maxHeight: QUICK_EDIT_POPOVER_MAX_HEIGHT,
116
- overflowY: 'auto',
117
- overscrollBehavior: 'contain',
276
+ ...bodyStyles,
118
277
  },
119
278
  },
120
- content: (popover) => {
121
- model.viewContainer = popover;
122
- model.__onSubmitSuccess = onSuccess;
123
- model._fieldProps = fieldProps;
124
- model._onOk = onOk;
125
- console.log('QuickEditFormModel.open3', Date.now() - model.now);
126
- return (
127
- <FlowModelRenderer
128
- fallback={<Skeleton.Input size="small" />}
129
- model={model}
130
- inputArgs={{ filterByTk, record, sourceFieldModelUid }}
131
- />
132
- );
133
- },
279
+ content,
134
280
  });
135
281
  }
136
282
 
@@ -171,15 +317,15 @@ export class QuickEditFormModel extends FlowModel {
171
317
  }
172
318
 
173
319
  render() {
174
- console.log('QuickEditFormModel.open4', Date.now() - this.now);
175
-
320
+ const isMobileLayout = this.context.isMobileLayout;
176
321
  return (
177
322
  <FormComponent model={this}>
178
323
  <div
179
324
  style={{
180
325
  minHeight: 0,
181
326
  overflowY: 'auto',
182
- maxHeight: QUICK_EDIT_FORM_MAX_HEIGHT,
327
+ maxHeight: isMobileLayout ? QUICK_EDIT_MOBILE_FORM_MAX_HEIGHT : QUICK_EDIT_FORM_MAX_HEIGHT,
328
+ padding: isMobileLayout ? QUICK_EDIT_MOBILE_CONTENT_PADDING : undefined,
183
329
  }}
184
330
  >
185
331
  {this.mapSubModels('fields', (field) => {
@@ -196,7 +342,14 @@ export class QuickEditFormModel extends FlowModel {
196
342
  );
197
343
  })}
198
344
  </div>
199
- <Space style={{ display: 'flex', justifyContent: 'flex-end', flexShrink: 0 }}>
345
+ <Space
346
+ style={{
347
+ display: 'flex',
348
+ justifyContent: 'flex-end',
349
+ flexShrink: 0,
350
+ padding: isMobileLayout ? QUICK_EDIT_MOBILE_ACTIONS_PADDING : undefined,
351
+ }}
352
+ >
200
353
  <Button
201
354
  onClick={() => {
202
355
  this.viewContainer.close();
@@ -465,11 +465,17 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
465
465
 
466
466
  it('builds non-empty contextParams for ctx.formValues.* deep association path', async () => {
467
467
  const model = await setupFormModel();
468
+ const sessionPayload = Buffer.from(JSON.stringify({ userId: 1, signInTime: 'form-record-slots' })).toString(
469
+ 'base64url',
470
+ );
468
471
  // 注入 api mock 到引擎上下文,拦截 variables:resolve 的请求
469
472
  const api = {
473
+ auth: { token: `test.${sessionPayload}.sig` },
470
474
  request: vi.fn(async (config: any) => {
471
- const payload = config?.data?.values || {};
472
- const batch = payload.batch || [];
475
+ const requestValues = config?.data?.values || {};
476
+ const batch = requestValues.batch || [];
477
+ expect(batch[0]?.rd).toEqual(expect.any(String));
478
+ expect(batch[0]?.template).toEqual({ who: '{{ ctx.formValues.assignees.org.name }}' });
473
479
  const cp = batch[0]?.contextParams || {};
474
480
  const keys = Object.keys(cp).sort();
475
481
  // 聚合为单键,不再使用索引键
@@ -7,17 +7,365 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, it, expect, vi, beforeEach } from 'vitest';
11
- import { FlowEngine, SingleRecordResource } from '@nocobase/flow-engine';
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
+ import { FlowEngine, FlowModel, SingleRecordResource } from '@nocobase/flow-engine';
12
12
  import { QuickEditFormModel } from '../QuickEditFormModel';
13
13
 
14
14
  describe('QuickEditFormModel - quick edit save triggers API (regression)', () => {
15
15
  let engine: FlowEngine;
16
+ const originalMatchMediaDescriptor = Object.getOwnPropertyDescriptor(window, 'matchMedia');
16
17
 
17
18
  beforeEach(() => {
18
19
  engine = new FlowEngine();
19
20
  });
20
21
 
22
+ afterEach(() => {
23
+ if (originalMatchMediaDescriptor) {
24
+ Object.defineProperty(window, 'matchMedia', originalMatchMediaDescriptor);
25
+ return;
26
+ }
27
+ delete (window as unknown as { matchMedia?: Window['matchMedia'] }).matchMedia;
28
+ });
29
+
30
+ const mockMatchMedia = (matches: boolean) => {
31
+ const matchMediaMock = vi.fn((query: string) => {
32
+ return {
33
+ matches,
34
+ media: query,
35
+ onchange: null,
36
+ addListener: vi.fn(),
37
+ removeListener: vi.fn(),
38
+ addEventListener: vi.fn(),
39
+ removeEventListener: vi.fn(),
40
+ dispatchEvent: vi.fn(),
41
+ } as unknown as MediaQueryList;
42
+ });
43
+
44
+ Object.defineProperty(window, 'matchMedia', {
45
+ ...(originalMatchMediaDescriptor || { configurable: true, writable: true }),
46
+ value: matchMediaMock,
47
+ });
48
+ };
49
+
50
+ const addUsersCollection = () => {
51
+ const ds = engine.context.dataSourceManager.getDataSource('main');
52
+ ds.addCollection({
53
+ name: 'users',
54
+ fields: [{ name: 'name', type: 'string', interface: 'input', uiSchema: { title: 'Name' } }],
55
+ });
56
+ };
57
+
58
+ it('uses source field context when opening quick edit', async () => {
59
+ engine.registerModels({ QuickEditFormModel });
60
+ engine.context.defineProperty('pageActive', { value: { value: false } });
61
+ engine.context.defineProperty('viewer', { value: { open: vi.fn(async () => undefined) } });
62
+
63
+ const page = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'page' });
64
+ page.context.defineProperty('pageActive', { value: { value: true } });
65
+ const source = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'source-field', parentId: page.uid });
66
+
67
+ await QuickEditFormModel.open({
68
+ flowEngine: engine,
69
+ target: document.createElement('div'),
70
+ dataSourceKey: 'main',
71
+ collectionName: 'users',
72
+ fieldPath: 'name',
73
+ record: {},
74
+ sourceFieldModelUid: source.uid,
75
+ });
76
+
77
+ let quickEditModel: QuickEditFormModel | undefined;
78
+ engine.forEachModel((model) => {
79
+ if (model instanceof QuickEditFormModel) {
80
+ quickEditModel = model;
81
+ }
82
+ });
83
+
84
+ expect(quickEditModel?.context.pageActive.value).toBe(true);
85
+ });
86
+
87
+ it('opens quick edit in a mobile drawer when the source field layout is mobile', async () => {
88
+ engine.registerModels({ QuickEditFormModel });
89
+ addUsersCollection();
90
+ const engineOpen = vi.fn(async () => undefined);
91
+ const sourceOpen = vi.fn(async () => undefined);
92
+ engine.context.defineProperty('isMobileLayout', { value: false });
93
+ engine.context.defineProperty('viewer', { value: { open: engineOpen } });
94
+ const source = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'source-field' });
95
+ source.context.defineProperty('isMobileLayout', { value: true });
96
+ source.context.defineProperty('viewer', { value: { open: sourceOpen } });
97
+
98
+ await QuickEditFormModel.open({
99
+ flowEngine: engine,
100
+ target: document.createElement('div'),
101
+ dataSourceKey: 'main',
102
+ collectionName: 'users',
103
+ fieldPath: 'name',
104
+ record: {},
105
+ sourceFieldModelUid: source.uid,
106
+ });
107
+
108
+ expect(engineOpen).not.toHaveBeenCalled();
109
+ expect(sourceOpen).toHaveBeenCalledTimes(1);
110
+ expect(sourceOpen).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ type: 'drawer',
113
+ title: 'Name',
114
+ closable: true,
115
+ placement: 'bottom',
116
+ styles: {
117
+ body: expect.objectContaining({
118
+ maxHeight: '50vh',
119
+ }),
120
+ },
121
+ inputArgs: {
122
+ isMobileLayout: true,
123
+ },
124
+ }),
125
+ );
126
+ let quickEditModel: QuickEditFormModel | undefined;
127
+ engine.forEachModel((model) => {
128
+ if (model instanceof QuickEditFormModel) {
129
+ quickEditModel = model;
130
+ }
131
+ });
132
+ expect(quickEditModel?.context.isMobileLayout).toBe(true);
133
+ });
134
+
135
+ it('uses the source table column title for the mobile drawer header', async () => {
136
+ engine.registerModels({ QuickEditFormModel });
137
+ addUsersCollection();
138
+ const sourceOpen = vi.fn(async () => undefined);
139
+ engine.context.defineProperty('viewer', { value: { open: vi.fn(async () => undefined) } });
140
+ const column = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'table-column' });
141
+ column.setProps({ title: 'Custom marital status' });
142
+ const source = engine.createModel<FlowModel>({ use: 'FlowModel', uid: 'source-field', parentId: column.uid });
143
+ source.context.defineProperty('isMobileLayout', { value: true });
144
+ source.context.defineProperty('viewer', { value: { open: sourceOpen } });
145
+
146
+ await QuickEditFormModel.open({
147
+ flowEngine: engine,
148
+ target: document.createElement('div'),
149
+ dataSourceKey: 'main',
150
+ collectionName: 'users',
151
+ fieldPath: 'name',
152
+ record: {},
153
+ sourceFieldModelUid: source.uid,
154
+ });
155
+
156
+ expect(sourceOpen).toHaveBeenCalledWith(
157
+ expect.objectContaining({
158
+ type: 'drawer',
159
+ title: 'Custom marital status',
160
+ }),
161
+ );
162
+ });
163
+
164
+ it('uses the mobile drawer fallback on narrow viewports', async () => {
165
+ engine.registerModels({ QuickEditFormModel });
166
+ addUsersCollection();
167
+ mockMatchMedia(true);
168
+ const viewContext = {
169
+ defineProperty: vi.fn(),
170
+ };
171
+ const open = vi.fn(
172
+ async (config: { content: (view: { close: () => void }, context: typeof viewContext) => unknown }) => {
173
+ config.content({ close: vi.fn() }, viewContext);
174
+ },
175
+ );
176
+ engine.context.defineProperty('viewer', { value: { open } });
177
+
178
+ await QuickEditFormModel.open({
179
+ flowEngine: engine,
180
+ target: document.createElement('div'),
181
+ dataSourceKey: 'main',
182
+ collectionName: 'users',
183
+ fieldPath: 'name',
184
+ record: {},
185
+ });
186
+
187
+ expect(open).toHaveBeenCalledTimes(1);
188
+ expect(viewContext.defineProperty).toHaveBeenCalledWith('isMobileLayout', { value: true });
189
+ expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 768px)');
190
+ expect(open).toHaveBeenCalledWith(
191
+ expect.objectContaining({
192
+ type: 'drawer',
193
+ title: 'Name',
194
+ closable: true,
195
+ placement: 'bottom',
196
+ styles: {
197
+ body: expect.objectContaining({
198
+ maxHeight: '50vh',
199
+ }),
200
+ },
201
+ inputArgs: {
202
+ isMobileLayout: true,
203
+ },
204
+ }),
205
+ );
206
+ let quickEditModel: QuickEditFormModel | undefined;
207
+ engine.forEachModel((model) => {
208
+ if (model instanceof QuickEditFormModel) {
209
+ quickEditModel = model;
210
+ }
211
+ });
212
+ expect(quickEditModel?.context.isMobileLayout).toBe(true);
213
+ });
214
+
215
+ it('uses the engine mobile layout context without relying on matchMedia', async () => {
216
+ engine.registerModels({ QuickEditFormModel });
217
+ addUsersCollection();
218
+ mockMatchMedia(false);
219
+ const open = vi.fn(async () => undefined);
220
+ engine.context.defineProperty('isMobileLayout', { value: true });
221
+ engine.context.defineProperty('viewer', { value: { open } });
222
+
223
+ await QuickEditFormModel.open({
224
+ flowEngine: engine,
225
+ target: document.createElement('div'),
226
+ dataSourceKey: 'main',
227
+ collectionName: 'users',
228
+ fieldPath: 'name',
229
+ record: {},
230
+ });
231
+
232
+ expect(window.matchMedia).not.toHaveBeenCalled();
233
+ expect(open).toHaveBeenCalledTimes(1);
234
+ expect(open).toHaveBeenCalledWith(
235
+ expect.objectContaining({
236
+ type: 'drawer',
237
+ title: 'Name',
238
+ placement: 'bottom',
239
+ }),
240
+ );
241
+ });
242
+
243
+ it('keeps an explicit non-mobile layout context ahead of the narrow viewport fallback', async () => {
244
+ engine.registerModels({ QuickEditFormModel });
245
+ addUsersCollection();
246
+ mockMatchMedia(true);
247
+ const open = vi.fn(async () => undefined);
248
+ const target = document.createElement('div');
249
+ engine.context.defineProperty('isMobileLayout', { value: false });
250
+ engine.context.defineProperty('viewer', { value: { open } });
251
+
252
+ await QuickEditFormModel.open({
253
+ flowEngine: engine,
254
+ target,
255
+ dataSourceKey: 'main',
256
+ collectionName: 'users',
257
+ fieldPath: 'name',
258
+ record: {},
259
+ });
260
+
261
+ expect(window.matchMedia).not.toHaveBeenCalled();
262
+ expect(open).toHaveBeenCalledTimes(1);
263
+ expect(open).toHaveBeenCalledWith(
264
+ expect.objectContaining({
265
+ type: 'popover',
266
+ target,
267
+ placement: 'rightTop',
268
+ }),
269
+ );
270
+ });
271
+
272
+ it('keeps dynamic preventClose effective for mobile quick edit drawer containers', async () => {
273
+ engine.registerModels({ QuickEditFormModel });
274
+ addUsersCollection();
275
+ mockMatchMedia(true);
276
+ const originalBeforeClose = vi.fn(async () => true);
277
+ const drawerView = {
278
+ close: vi.fn(),
279
+ update: vi.fn(),
280
+ beforeClose: originalBeforeClose,
281
+ };
282
+ const open = vi.fn(async (config: { content: (view: typeof drawerView) => unknown }) => {
283
+ config.content(drawerView);
284
+ });
285
+ engine.context.defineProperty('viewer', { value: { open } });
286
+
287
+ await QuickEditFormModel.open({
288
+ flowEngine: engine,
289
+ target: document.createElement('div'),
290
+ dataSourceKey: 'main',
291
+ collectionName: 'users',
292
+ fieldPath: 'name',
293
+ record: {},
294
+ });
295
+
296
+ let quickEditModel: QuickEditFormModel | undefined;
297
+ engine.forEachModel((model) => {
298
+ if (model instanceof QuickEditFormModel) {
299
+ quickEditModel = model;
300
+ }
301
+ });
302
+
303
+ quickEditModel?.viewContainer.update?.({ preventClose: true });
304
+ expect(drawerView.update).toHaveBeenCalledWith({ preventClose: true });
305
+ await expect(drawerView.beforeClose?.({ force: false })).resolves.toBe(false);
306
+ expect(originalBeforeClose).not.toHaveBeenCalled();
307
+
308
+ quickEditModel?.viewContainer.update?.({ preventClose: false });
309
+ expect(drawerView.update).toHaveBeenCalledWith({ preventClose: false });
310
+ await expect(drawerView.beforeClose?.({ force: false })).resolves.toBe(true);
311
+ expect(originalBeforeClose).toHaveBeenCalledTimes(1);
312
+ });
313
+
314
+ it('keeps desktop quick edit in the existing right-top popover', async () => {
315
+ engine.registerModels({ QuickEditFormModel });
316
+ mockMatchMedia(false);
317
+ const open = vi.fn(async () => undefined);
318
+ const target = document.createElement('div');
319
+ engine.context.defineProperty('viewer', { value: { open } });
320
+
321
+ await QuickEditFormModel.open({
322
+ flowEngine: engine,
323
+ target,
324
+ dataSourceKey: 'main',
325
+ collectionName: 'users',
326
+ fieldPath: 'name',
327
+ record: {},
328
+ });
329
+
330
+ expect(open).toHaveBeenCalledTimes(1);
331
+ expect(open).toHaveBeenCalledWith(
332
+ expect.objectContaining({
333
+ type: 'popover',
334
+ target,
335
+ placement: 'rightTop',
336
+ styles: {
337
+ body: expect.objectContaining({
338
+ width: 420,
339
+ }),
340
+ },
341
+ }),
342
+ );
343
+ });
344
+
345
+ it('keeps mobile quick edit content naturally sized with half-window scroll bounds', () => {
346
+ engine.registerModels({ QuickEditFormModel });
347
+ const model = engine.createModel<QuickEditFormModel>({
348
+ use: QuickEditFormModel,
349
+ uid: 'quick-edit-style',
350
+ });
351
+ model.context.defineProperty('isMobileLayout', { value: true });
352
+
353
+ const result = QuickEditFormModel.prototype.render.call(model) as any;
354
+ const formBody = result.props.children[0];
355
+ const actions = result.props.children[1];
356
+
357
+ expect(formBody.props.style).toMatchObject({
358
+ maxHeight: 'calc(50vh - var(--nb-mobile-page-header-height, 40px) - 132px)',
359
+ overflowY: 'auto',
360
+ padding: '8px var(--nb-mobile-page-tabs-content-padding, 12px) 0',
361
+ });
362
+ expect(formBody.props.style.minHeight).toBe(0);
363
+ expect(actions.props.style).toMatchObject({
364
+ justifyContent: 'flex-end',
365
+ padding: '8px var(--nb-mobile-page-tabs-content-padding, 12px) calc(80px + env(safe-area-inset-bottom, 0px))',
366
+ });
367
+ });
368
+
21
369
  it('calls update with filterByTk and merges primary key from ctx.collection/record', async () => {
22
370
  // 1) 准备数据源与集合(含主键字段)
23
371
  const dsm = engine.context.dataSourceManager;