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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/PluginSettingsManager.d.ts +33 -0
  4. package/es/RouteRepository.d.ts +29 -6
  5. package/es/RouterManager.d.ts +15 -0
  6. package/es/authRedirect.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  8. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  9. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  10. package/es/components/category-tabs/index.d.ts +9 -0
  11. package/es/components/form/JsonTextArea.d.ts +2 -1
  12. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  13. package/es/components/form/TypedVariableInput.d.ts +31 -5
  14. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  15. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  16. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  17. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  18. package/es/components/form/filter/index.d.ts +2 -0
  19. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  20. package/es/components/form/index.d.ts +1 -0
  21. package/es/components/index.d.ts +2 -0
  22. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  23. package/es/entry-actions/index.d.ts +9 -0
  24. package/es/flow/actions/afterSuccess.d.ts +8 -1
  25. package/es/flow/actions/index.d.ts +2 -2
  26. package/es/flow/actions/linkageRules.d.ts +2 -0
  27. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  28. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  29. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  30. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  31. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
  32. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  33. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  34. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  35. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  36. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  37. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  38. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  39. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  40. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  41. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  42. package/es/flow/components/FlowRoute.d.ts +2 -2
  43. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  44. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  45. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  46. package/es/flow/index.d.ts +1 -0
  47. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  48. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  49. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  50. package/es/flow/models/base/GridModel.d.ts +1 -1
  51. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  52. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  53. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  54. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  55. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  56. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  57. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  58. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  59. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  60. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  61. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  62. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  63. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  64. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  65. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  66. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  67. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  68. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  69. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  70. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  71. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  72. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  73. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  74. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  75. package/es/flow/utils/formValueDeps.d.ts +32 -0
  76. package/es/flow-compat/FieldValidation.d.ts +2 -1
  77. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  78. package/es/flow-compat/routeTypes.d.ts +1 -0
  79. package/es/index.d.ts +8 -1
  80. package/es/index.mjs +367 -181
  81. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  82. package/es/utils/index.d.ts +1 -0
  83. package/es/utils/markdownSanitize.d.ts +11 -0
  84. package/lib/index.js +367 -181
  85. package/lib/locale/languageCodes.js +3 -1
  86. package/package.json +9 -7
  87. package/src/APIClient.ts +1 -10
  88. package/src/Application.tsx +4 -0
  89. package/src/BaseApplication.tsx +13 -6
  90. package/src/PluginSettingsManager.ts +53 -0
  91. package/src/RouteRepository.ts +151 -24
  92. package/src/RouterManager.tsx +142 -1
  93. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  94. package/src/__tests__/RouteRepository.test.ts +239 -0
  95. package/src/__tests__/RouterManager.test.ts +125 -0
  96. package/src/__tests__/app.test.tsx +73 -0
  97. package/src/__tests__/authRedirect.test.ts +17 -0
  98. package/src/__tests__/browserChecker.test.ts +61 -0
  99. package/src/__tests__/exports.test.ts +16 -0
  100. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  101. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +234 -2
  102. package/src/__tests__/plugin-manager.test.tsx +44 -2
  103. package/src/__tests__/settings-center.test.tsx +40 -1
  104. package/src/authRedirect.ts +38 -0
  105. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  106. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  107. package/src/collection-manager/field-configure.ts +1 -1
  108. package/src/collection-manager/field-validation.ts +1 -1
  109. package/src/collection-manager/interfaces/id.ts +1 -1
  110. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  111. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  112. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  113. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  114. package/src/collection-manager/interfaces/obo.tsx +2 -2
  115. package/src/collection-manager/interfaces/oho.tsx +2 -2
  116. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  117. package/src/collection-manager/interfaces/uuid.ts +1 -1
  118. package/src/components/AppComponents.tsx +19 -3
  119. package/src/components/README.md +7 -1
  120. package/src/components/README.zh-CN.md +6 -1
  121. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  122. package/src/components/category-tabs/index.ts +10 -0
  123. package/src/components/form/JsonTextArea.tsx +25 -11
  124. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  125. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  126. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  127. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  128. package/src/components/form/TypedVariableInput.tsx +452 -101
  129. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  130. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  131. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  132. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  133. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  134. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  135. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  136. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  137. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  138. package/src/components/form/filter/index.ts +2 -0
  139. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  140. package/src/components/form/index.tsx +1 -0
  141. package/src/components/index.ts +2 -0
  142. package/src/entry-actions/EntryActionManager.ts +76 -0
  143. package/src/entry-actions/index.ts +10 -0
  144. package/src/flow/FlowPage.tsx +38 -3
  145. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  146. package/src/flow/__tests__/FlowRoute.test.tsx +1181 -80
  147. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  148. package/src/flow/__tests__/getKey.test.ts +7 -0
  149. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  150. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +115 -0
  151. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  152. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  153. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1124 -0
  154. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  155. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  156. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  157. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  158. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  159. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  160. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  161. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  162. package/src/flow/actions/afterSuccess.tsx +142 -3
  163. package/src/flow/actions/customVariable.tsx +1 -2
  164. package/src/flow/actions/dataScopeFilter.ts +20 -1
  165. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  166. package/src/flow/actions/index.ts +3 -1
  167. package/src/flow/actions/linkageRules.tsx +209 -47
  168. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  169. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  170. package/src/flow/actions/openView.tsx +59 -5
  171. package/src/flow/actions/runjs.tsx +2 -14
  172. package/src/flow/actions/validation.tsx +62 -30
  173. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  174. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  175. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  176. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -14
  177. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  178. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  179. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  180. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  181. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  182. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  183. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  184. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  185. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  186. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  187. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
  188. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  189. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  190. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  191. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  192. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  193. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  194. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  195. package/src/flow/common/Markdown/Display.tsx +14 -3
  196. package/src/flow/common/Markdown/Edit.tsx +19 -7
  197. package/src/flow/common/Markdown/style.ts +4 -0
  198. package/src/flow/components/BlockItemCard.tsx +2 -2
  199. package/src/flow/components/DefaultValue.tsx +1 -2
  200. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  201. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  202. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  203. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  204. package/src/flow/components/FlowRoute.tsx +256 -53
  205. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  206. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  207. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  208. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  209. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  210. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  211. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  212. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  213. package/src/flow/components/fieldAssignOptions.ts +155 -27
  214. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  215. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  216. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  217. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  218. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  219. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  220. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  221. package/src/flow/index.ts +4 -1
  222. package/src/flow/internal/components/Markdown/util.ts +2 -1
  223. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  224. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  225. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  226. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  227. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  228. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  229. package/src/flow/models/base/ActionModel.tsx +21 -8
  230. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  231. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  232. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  233. package/src/flow/models/base/GridModel.tsx +38 -7
  234. package/src/flow/models/base/PageModel/PageModel.tsx +387 -50
  235. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  236. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  237. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +817 -13
  238. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  239. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  240. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  241. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  242. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  243. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  244. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  245. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  246. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  247. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +59 -6
  248. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  249. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  250. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  251. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +143 -4
  252. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  253. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  254. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  255. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  256. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  257. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  258. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  259. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  260. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  261. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  262. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  263. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  264. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  265. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  266. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  267. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  268. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  269. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  270. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +38 -14
  271. package/src/flow/models/blocks/table/TableBlockModel.tsx +42 -19
  272. package/src/flow/models/blocks/table/TableColumnModel.tsx +72 -6
  273. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  274. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  275. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  276. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  277. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  278. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +243 -1
  279. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  280. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  281. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  282. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  283. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +31 -7
  284. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  285. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  286. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  287. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  288. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  289. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  290. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  291. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  292. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  293. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  294. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  295. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  296. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  297. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  298. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  299. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  300. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  301. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  302. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  303. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  304. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  305. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  306. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  307. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  308. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  309. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  310. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  311. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  312. package/src/flow/routeTransientInputArgs.ts +27 -0
  313. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  314. package/src/flow/utils/dataScopeFormValueClear.ts +219 -81
  315. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  316. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  317. package/src/flow/utils/formValueDeps.ts +170 -0
  318. package/src/flow-compat/FieldValidation.tsx +122 -60
  319. package/src/flow-compat/Popover.tsx +43 -4
  320. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  321. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  322. package/src/flow-compat/routeTypes.ts +1 -0
  323. package/src/index.ts +14 -1
  324. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  325. package/src/locale/languageCodes.ts +3 -1
  326. package/src/nocobase-buildin-plugin/index.tsx +63 -15
  327. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  328. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  329. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  330. package/src/settings-center/plugin-manager/index.tsx +3 -0
  331. package/src/settings-center/utils.tsx +0 -6
  332. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  333. package/src/utils/index.tsx +1 -0
  334. package/src/utils/markdownSanitize.ts +88 -0
@@ -0,0 +1,83 @@
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 { FlowEngine, FlowRuntimeContext } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { ActionModel } from '../ActionModel';
13
+
14
+ class IconActionModel extends ActionModel {
15
+ defaultProps = {
16
+ type: 'default' as const,
17
+ title: 'Icon action',
18
+ icon: 'PlusOutlined',
19
+ };
20
+ }
21
+
22
+ class PlainActionModel extends ActionModel {
23
+ defaultProps = {
24
+ type: 'default' as const,
25
+ title: 'Plain action',
26
+ };
27
+ }
28
+
29
+ function getButtonSettingsDefaultParams(model: ActionModel) {
30
+ const defaultParams = model.getFlow('buttonSettings')?.getStep('general')?.defaultParams;
31
+ if (typeof defaultParams !== 'function') {
32
+ throw new Error('buttonSettings.general.defaultParams is unavailable.');
33
+ }
34
+ return defaultParams(new FlowRuntimeContext(model, 'buttonSettings', 'settings'));
35
+ }
36
+
37
+ describe('ActionModel button settings', () => {
38
+ it('uses icon-only buttons by default for mobile icon actions', () => {
39
+ const engine = new FlowEngine();
40
+ engine.registerModels({ IconActionModel });
41
+ const model = engine.createModel<IconActionModel>({
42
+ use: 'IconActionModel',
43
+ uid: 'mobile-icon-action',
44
+ });
45
+
46
+ model.context.defineProperty('isMobileLayout', { value: true });
47
+
48
+ expect(getButtonSettingsDefaultParams(model)).toMatchObject({
49
+ icon: 'PlusOutlined',
50
+ iconOnly: true,
51
+ });
52
+ });
53
+
54
+ it('keeps button titles visible by default outside mobile layouts', () => {
55
+ const engine = new FlowEngine();
56
+ engine.registerModels({ IconActionModel });
57
+ const model = engine.createModel<IconActionModel>({
58
+ use: 'IconActionModel',
59
+ uid: 'desktop-icon-action',
60
+ });
61
+
62
+ expect(getButtonSettingsDefaultParams(model)).toMatchObject({
63
+ icon: 'PlusOutlined',
64
+ });
65
+ expect(getButtonSettingsDefaultParams(model)).not.toHaveProperty('iconOnly', true);
66
+ });
67
+
68
+ it('does not force icon-only buttons for mobile actions without icons', () => {
69
+ const engine = new FlowEngine();
70
+ engine.registerModels({ PlainActionModel });
71
+ const model = engine.createModel<PlainActionModel>({
72
+ use: 'PlainActionModel',
73
+ uid: 'mobile-plain-action',
74
+ });
75
+
76
+ model.context.defineProperty('isMobileLayout', { value: true });
77
+
78
+ expect(getButtonSettingsDefaultParams(model)).toMatchObject({
79
+ title: 'Plain action',
80
+ });
81
+ expect(getButtonSettingsDefaultParams(model)).not.toHaveProperty('iconOnly', true);
82
+ });
83
+ });
@@ -0,0 +1,37 @@
1
+ import { render, screen } from '@nocobase/test/client';
2
+ import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
3
+ import { App, ConfigProvider } from 'antd';
4
+ import React from 'react';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { ActionModel } from '../ActionModel';
7
+
8
+ class NoIconActionModel extends ActionModel {
9
+ defaultProps = {
10
+ type: 'link' as const,
11
+ title: 'Open details',
12
+ };
13
+ }
14
+
15
+ describe('ActionModel rendering', () => {
16
+ it('shows the title when iconOnly is true but no icon is configured', () => {
17
+ const engine = new FlowEngine();
18
+ engine.registerModels({ NoIconActionModel });
19
+ const model = engine.createModel<NoIconActionModel>({
20
+ use: 'NoIconActionModel',
21
+ props: {
22
+ title: 'Open details',
23
+ iconOnly: true,
24
+ },
25
+ });
26
+
27
+ render(
28
+ <FlowEngineProvider engine={engine}>
29
+ <ConfigProvider>
30
+ <App>{model.render()}</App>
31
+ </ConfigProvider>
32
+ </FlowEngineProvider>,
33
+ );
34
+
35
+ expect(screen.getByRole('button', { name: 'Open details' })).toBeInTheDocument();
36
+ });
37
+ });
@@ -0,0 +1,124 @@
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 { FlowEngine, type FlowModelContext, MultiRecordResource, VIEW_ACTIVATED_EVENT } from '@nocobase/flow-engine';
11
+ import { beforeEach, describe, expect, it } from 'vitest';
12
+ import { BlockGridModel } from '../BlockGridModel';
13
+ import { CollectionBlockModel } from '../CollectionBlockModel';
14
+
15
+ class TestBlockGridModel extends BlockGridModel {
16
+ mountForTest() {
17
+ super.onMount();
18
+ }
19
+
20
+ unmountForTest() {
21
+ super.onUnmount();
22
+ }
23
+ }
24
+
25
+ class TestMultiRecordResource extends MultiRecordResource<Record<string, unknown>> {
26
+ refreshCalls = 0;
27
+
28
+ override async refresh(): Promise<void> {
29
+ this.refreshCalls += 1;
30
+ this.emit('refresh');
31
+ }
32
+ }
33
+
34
+ class TestCollectionBlockModel extends CollectionBlockModel {
35
+ createResource(ctx: FlowModelContext) {
36
+ return ctx.createResource(TestMultiRecordResource);
37
+ }
38
+
39
+ mountForTest() {
40
+ super.onMount();
41
+ }
42
+ }
43
+
44
+ describe('BlockGridModel - select scene activation', () => {
45
+ let engine: FlowEngine;
46
+
47
+ beforeEach(() => {
48
+ engine = new FlowEngine();
49
+ engine.context.defineProperty('location', { value: { search: '' } });
50
+ engine.registerModels({ TestBlockGridModel, TestCollectionBlockModel });
51
+
52
+ engine.dataSourceManager.getDataSource('main').addCollection({
53
+ name: 'roles',
54
+ filterTargetKey: 'id',
55
+ fields: [
56
+ { name: 'id', type: 'integer', interface: 'number' },
57
+ { name: 'name', type: 'string', interface: 'input' },
58
+ ],
59
+ });
60
+ });
61
+
62
+ function createModels(scene = 'select') {
63
+ engine.context.defineProperty('view', { value: { inputArgs: { scene } } });
64
+ const grid = engine.createModel<TestBlockGridModel>({ use: 'TestBlockGridModel' });
65
+ const block = grid.addSubModel<TestCollectionBlockModel>('items', {
66
+ use: 'TestCollectionBlockModel',
67
+ stepParams: {
68
+ resourceSettings: {
69
+ init: {
70
+ dataSourceKey: 'main',
71
+ collectionName: 'roles',
72
+ },
73
+ },
74
+ },
75
+ });
76
+ const resource = block.context.resource as TestMultiRecordResource;
77
+
78
+ block.mountForTest();
79
+ grid.mountForTest();
80
+ return { grid, resource };
81
+ }
82
+
83
+ it('refreshes dirty collection blocks once when a nested view closes', async () => {
84
+ const { grid, resource } = createModels();
85
+
86
+ engine.markDataSourceDirty('main', 'roles');
87
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
88
+ await Promise.resolve();
89
+
90
+ expect(resource.refreshCalls).toBe(1);
91
+
92
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
93
+ await Promise.resolve();
94
+
95
+ expect(resource.refreshCalls).toBe(1);
96
+ grid.unmountForTest();
97
+ });
98
+
99
+ it('does not refresh without dirty data or after the grid unmounts', async () => {
100
+ const { grid, resource } = createModels();
101
+
102
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
103
+ await Promise.resolve();
104
+ expect(resource.refreshCalls).toBe(0);
105
+
106
+ grid.unmountForTest();
107
+ engine.markDataSourceDirty('main', 'roles');
108
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
109
+ await Promise.resolve();
110
+
111
+ expect(resource.refreshCalls).toBe(0);
112
+ });
113
+
114
+ it('does not handle activation outside the select scene', async () => {
115
+ const { grid, resource } = createModels('list');
116
+
117
+ engine.markDataSourceDirty('main', 'roles');
118
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
119
+ await Promise.resolve();
120
+
121
+ expect(resource.refreshCalls).toBe(0);
122
+ grid.unmountForTest();
123
+ });
124
+ });
@@ -0,0 +1,41 @@
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 { FlowEngine, FlowModelContext, MultiRecordResource } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { CollectionBlockModel } from '../CollectionBlockModel';
13
+
14
+ class AddAppendsCollectionBlockModel extends CollectionBlockModel {
15
+ createResource(ctx: FlowModelContext) {
16
+ return ctx.createResource(MultiRecordResource);
17
+ }
18
+ }
19
+
20
+ describe('CollectionBlockModel addAppends', () => {
21
+ it('does not throw for a nested field path when the collection is unavailable', () => {
22
+ const engine = new FlowEngine();
23
+ engine.registerModels({ AddAppendsCollectionBlockModel });
24
+
25
+ const model = engine.createModel<AddAppendsCollectionBlockModel>({
26
+ uid: 'missing-collection-add-appends-block',
27
+ use: 'AddAppendsCollectionBlockModel',
28
+ stepParams: {
29
+ resourceSettings: {
30
+ init: {
31
+ dataSourceKey: 'main',
32
+ collectionName: 'missing_collection',
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ expect(model.collection).toBeUndefined();
39
+ expect(() => model.addAppends('createdBy.departments')).not.toThrow();
40
+ });
41
+ });
@@ -10,7 +10,7 @@
10
10
  import { FlowEngine } from '@nocobase/flow-engine';
11
11
  import React from 'react';
12
12
  import { beforeEach, describe, expect, it, vi } from 'vitest';
13
- import { GridModel } from '../GridModel';
13
+ import { GridModel, transformRowsToSingleColumn } from '../GridModel';
14
14
 
15
15
  const createMockRect = ({ top, left, width, height }: { top: number; left: number; width: number; height: number }) => {
16
16
  return {
@@ -66,6 +66,83 @@ const createGridContainer = () => {
66
66
  return container;
67
67
  };
68
68
 
69
+ const createGridContainerWithEmptyColumn = () => {
70
+ const container = document.createElement('div');
71
+ container.setAttribute('data-grid-root', '');
72
+ const row = document.createElement('div');
73
+ row.setAttribute('data-grid-row-id', 'row-1');
74
+ container.appendChild(row);
75
+
76
+ const firstColumn = document.createElement('div');
77
+ firstColumn.setAttribute('data-grid-column-row-id', 'row-1');
78
+ firstColumn.setAttribute('data-grid-column-index', '0');
79
+ row.appendChild(firstColumn);
80
+
81
+ const item = document.createElement('div');
82
+ item.setAttribute('data-grid-item-row-id', 'row-1');
83
+ item.setAttribute('data-grid-column-index', '0');
84
+ item.setAttribute('data-grid-item-index', '0');
85
+ item.setAttribute('data-grid-item-uid', 'item-1');
86
+ firstColumn.appendChild(item);
87
+
88
+ const secondColumn = document.createElement('div');
89
+ secondColumn.setAttribute('data-grid-column-row-id', 'row-1');
90
+ secondColumn.setAttribute('data-grid-column-index', '1');
91
+ row.appendChild(secondColumn);
92
+
93
+ mockRect(container, { top: 0, left: 0, width: 480, height: 280 });
94
+ mockRect(row, { top: 20, left: 20, width: 440, height: 120 });
95
+ mockRect(firstColumn, { top: 20, left: 20, width: 220, height: 120 });
96
+ mockRect(item, { top: 30, left: 30, width: 200, height: 100 });
97
+ mockRect(secondColumn, { top: 20, left: 260, width: 200, height: 120 });
98
+ return container;
99
+ };
100
+
101
+ const createProjectedMobileGridContainer = (rows: Record<string, string[][]>) => {
102
+ const container = document.createElement('div');
103
+ container.setAttribute('data-grid-root', '');
104
+ mockRect(container, { top: 0, left: 0, width: 480, height: 320 });
105
+
106
+ let top = 20;
107
+ Object.entries(transformRowsToSingleColumn(rows)).forEach(([rowId, columns]) => {
108
+ const row = document.createElement('div');
109
+ row.setAttribute('data-grid-row-id', rowId);
110
+ container.appendChild(row);
111
+
112
+ const column = document.createElement('div');
113
+ column.setAttribute('data-grid-column-row-id', rowId);
114
+ column.setAttribute('data-grid-column-index', '0');
115
+ row.appendChild(column);
116
+
117
+ const items = columns[0] || [];
118
+ items.forEach((itemUid, itemIndex) => {
119
+ const item = document.createElement('div');
120
+ item.setAttribute('data-grid-item-row-id', rowId);
121
+ item.setAttribute('data-grid-column-index', '0');
122
+ item.setAttribute('data-grid-item-index', String(itemIndex));
123
+ item.setAttribute('data-grid-item-uid', itemUid);
124
+ column.appendChild(item);
125
+ mockRect(item, { top: top + 10 + itemIndex * 36, left: 30, width: 420, height: 32 });
126
+ });
127
+
128
+ mockRect(row, { top, left: 20, width: 440, height: 52 });
129
+ mockRect(column, { top, left: 20, width: 440, height: 52 });
130
+ top += 72;
131
+ });
132
+
133
+ return container;
134
+ };
135
+
136
+ const setMobileGridItems = (engine: FlowEngine, model: GridModel, itemUids: string[]) => {
137
+ (model as any).subModels = {
138
+ items: itemUids.map((itemUid) => engine.createModel({ use: 'FlowModel', uid: itemUid })),
139
+ };
140
+ };
141
+
142
+ const getSavedTopLevelItemOrder = (model: GridModel) => {
143
+ return Object.values(model.props.rows || {}).map((columns: string[][]) => columns[0]?.[0]);
144
+ };
145
+
69
146
  describe('GridModel drag snapshot container', () => {
70
147
  let engine: FlowEngine;
71
148
 
@@ -106,6 +183,167 @@ describe('GridModel drag snapshot container', () => {
106
183
  model.handleDragCancel({} as any);
107
184
  });
108
185
 
186
+ it('filters multi-column drop slots from mobile drag snapshots', () => {
187
+ const model = engine.createModel<GridModel>({
188
+ use: 'GridModel',
189
+ uid: 'grid-mobile-drag-slots',
190
+ props: {
191
+ rows: {
192
+ 'row-1': [['item-1'], []],
193
+ },
194
+ sizes: {
195
+ 'row-1': [12, 12],
196
+ },
197
+ },
198
+ structure: {} as any,
199
+ });
200
+ model.context.defineProperty('isMobileLayout', { value: true });
201
+
202
+ const container = createGridContainerWithEmptyColumn();
203
+ (model.gridContainerRef as any).current = container;
204
+
205
+ model.handleDragStart({
206
+ active: { id: 'item-1' },
207
+ activatorEvent: { clientX: 80, clientY: 80 },
208
+ } as any);
209
+
210
+ const slotTypes = ((model as any).dragState?.slots || []).map((slot) => slot.type);
211
+ expect(slotTypes).toContain('column');
212
+ expect(slotTypes).toContain('row-gap');
213
+ expect(slotTypes).not.toContain('column-edge');
214
+ expect(slotTypes).not.toContain('item-edge');
215
+ expect(slotTypes).not.toContain('empty-column');
216
+ model.handleDragCancel({} as any);
217
+ });
218
+
219
+ it('keeps empty-row slot available for the first mobile grid item', () => {
220
+ const model = engine.createModel<GridModel>({
221
+ use: 'GridModel',
222
+ uid: 'grid-mobile-empty-drag-slots',
223
+ props: {},
224
+ structure: {} as any,
225
+ });
226
+ model.context.defineProperty('isMobileLayout', { value: true });
227
+
228
+ const container = document.createElement('div');
229
+ mockRect(container, { top: 0, left: 0, width: 480, height: 280 });
230
+ (model.gridContainerRef as any).current = container;
231
+
232
+ model.handleDragStart({
233
+ active: { id: 'item-1' },
234
+ activatorEvent: { clientX: 80, clientY: 80 },
235
+ } as any);
236
+
237
+ expect(((model as any).dragState?.slots || []).map((slot) => slot.type)).toEqual(['empty-row']);
238
+ model.handleDragCancel({} as any);
239
+ });
240
+
241
+ it('saves mobile vertical sorting when moving the second item above the first one', () => {
242
+ const rows = {
243
+ 'row-a': [['item-a']],
244
+ 'row-b': [['item-b']],
245
+ };
246
+ const model = engine.createModel<GridModel>({
247
+ use: 'GridModel',
248
+ uid: 'grid-mobile-sort-second-above-first',
249
+ props: {
250
+ rows,
251
+ sizes: {
252
+ 'row-a': [24],
253
+ 'row-b': [24],
254
+ },
255
+ rowOrder: ['row-a', 'row-b'],
256
+ },
257
+ structure: {} as any,
258
+ });
259
+ model.context.defineProperty('isMobileLayout', { value: true });
260
+ setMobileGridItems(engine, model, ['item-a', 'item-b']);
261
+ (model as any).saveStepParams = vi.fn();
262
+
263
+ const container = createProjectedMobileGridContainer(rows);
264
+ (model.gridContainerRef as any).current = container;
265
+
266
+ model.handleDragStart({
267
+ active: { id: 'item-b' },
268
+ activatorEvent: { clientX: 240, clientY: 118 },
269
+ } as any);
270
+ model.handleDragEnd({
271
+ delta: { x: 0, y: -112 },
272
+ } as any);
273
+
274
+ expect(getSavedTopLevelItemOrder(model)).toEqual(['item-b', 'item-a']);
275
+ });
276
+
277
+ it('saves mobile vertical sorting when moving the first item below the last one', () => {
278
+ const rows = {
279
+ 'row-a': [['item-a']],
280
+ 'row-b': [['item-b']],
281
+ 'row-c': [['item-c']],
282
+ };
283
+ const model = engine.createModel<GridModel>({
284
+ use: 'GridModel',
285
+ uid: 'grid-mobile-sort-first-below-last',
286
+ props: {
287
+ rows,
288
+ sizes: {
289
+ 'row-a': [24],
290
+ 'row-b': [24],
291
+ 'row-c': [24],
292
+ },
293
+ rowOrder: ['row-a', 'row-b', 'row-c'],
294
+ },
295
+ structure: {} as any,
296
+ });
297
+ model.context.defineProperty('isMobileLayout', { value: true });
298
+ setMobileGridItems(engine, model, ['item-a', 'item-b', 'item-c']);
299
+ (model as any).saveStepParams = vi.fn();
300
+
301
+ const container = createProjectedMobileGridContainer(rows);
302
+ (model.gridContainerRef as any).current = container;
303
+
304
+ model.handleDragStart({
305
+ active: { id: 'item-a' },
306
+ activatorEvent: { clientX: 240, clientY: 46 },
307
+ } as any);
308
+ model.handleDragEnd({
309
+ delta: { x: 0, y: 184 },
310
+ } as any);
311
+
312
+ expect(getSavedTopLevelItemOrder(model)).toEqual(['item-b', 'item-c', 'item-a']);
313
+ });
314
+
315
+ it('does not filter multi-column drop slots from desktop drag snapshots', () => {
316
+ const model = engine.createModel<GridModel>({
317
+ use: 'GridModel',
318
+ uid: 'grid-desktop-drag-slots',
319
+ props: {
320
+ rows: {
321
+ 'row-1': [['item-1'], []],
322
+ },
323
+ sizes: {
324
+ 'row-1': [12, 12],
325
+ },
326
+ },
327
+ structure: {} as any,
328
+ });
329
+
330
+ const container = createGridContainerWithEmptyColumn();
331
+ (model.gridContainerRef as any).current = container;
332
+
333
+ model.handleDragStart({
334
+ active: { id: 'item-1' },
335
+ activatorEvent: { clientX: 80, clientY: 80 },
336
+ } as any);
337
+
338
+ const slotTypes = ((model as any).dragState?.slots || []).map((slot) => slot.type);
339
+ expect(slotTypes).toContain('column');
340
+ expect(slotTypes).toContain('row-gap');
341
+ expect(slotTypes).toContain('column-edge');
342
+ expect(slotTypes).toContain('item-edge');
343
+ expect(slotTypes).toContain('empty-column');
344
+ model.handleDragCancel({} as any);
345
+ });
346
+
109
347
  it('uses native pointer position instead of scroll-adjusted drag delta', () => {
110
348
  const model = engine.createModel<GridModel>({
111
349
  use: 'GridModel',
@@ -35,6 +35,36 @@ describe('transformRowsToSingleColumn', () => {
35
35
  expect(all).toEqual([['a'], ['b', 'c'], ['d'], ['e']]);
36
36
  });
37
37
 
38
+ it('should preserve row ids for ordinary single-column mobile rows', () => {
39
+ const rows = {
40
+ row_a: [['a']],
41
+ row_b: [['b']],
42
+ };
43
+
44
+ expect(transformRowsToSingleColumn(rows)).toEqual(rows);
45
+ });
46
+
47
+ it('should avoid adding another mobile prefix to ordinary single-column projected rows', () => {
48
+ const rows = {
49
+ 'mobile:row_a:0': [['a']],
50
+ };
51
+
52
+ expect(Object.keys(transformRowsToSingleColumn(rows))).toEqual(['mobile:row_a:0']);
53
+ });
54
+
55
+ it('should keep split multi-column row keys stable and unique', () => {
56
+ const rows = {
57
+ row_a: [['a'], ['b']],
58
+ };
59
+ const result = transformRowsToSingleColumn(rows);
60
+ const keys = Object.keys(result);
61
+
62
+ expect(keys).toHaveLength(2);
63
+ expect(new Set(keys).size).toBe(2);
64
+ expect(keys.every((key) => key.startsWith('mobile:'))).toBe(true);
65
+ expect(Object.values(result).map((row) => row[0])).toEqual([['a'], ['b']]);
66
+ });
67
+
38
68
  it('should skip columns that only contain EMPTY_COLUMN_UID', () => {
39
69
  const rows = buildRows([[[EMPTY_COLUMN_UID], ['a']]]);
40
70
  const result = transformRowsToSingleColumn(rows);
@@ -55,4 +85,22 @@ describe('transformRowsToSingleColumn', () => {
55
85
  const all = Object.values(result).map((r) => r[0]);
56
86
  expect(all).toEqual([['x'], ['y']]);
57
87
  });
88
+
89
+ it('should keep generated row keys stable for the same input', () => {
90
+ const rows = buildRows([
91
+ [['a'], ['b']],
92
+ [['c'], ['d']],
93
+ ]);
94
+
95
+ expect(Object.keys(transformRowsToSingleColumn(rows))).toEqual(Object.keys(transformRowsToSingleColumn(rows)));
96
+ });
97
+
98
+ it('should keep existing row keys stable when appending new mobile rows', () => {
99
+ const before = buildRows([[['a'], ['b']]]);
100
+ const after = buildRows([[['a'], ['b']], [['c']]]);
101
+ const beforeKeys = Object.keys(transformRowsToSingleColumn(before));
102
+ const afterKeys = Object.keys(transformRowsToSingleColumn(after));
103
+
104
+ expect(afterKeys.slice(0, beforeKeys.length)).toEqual(beforeKeys);
105
+ });
58
106
  });