@multiplatform.one/frappe-ui 7.11.0 → 7.15.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 (341) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/FrappeDashboard.cjs +8 -10
  3. package/dist/cjs/FrappeDashboard.native.js +8 -14
  4. package/dist/cjs/FrappeDashboard.native.js.map +1 -1
  5. package/dist/cjs/FrappeForm.cjs +465 -175
  6. package/dist/cjs/FrappeForm.native.js +611 -207
  7. package/dist/cjs/FrappeForm.native.js.map +1 -1
  8. package/dist/cjs/FrappeFormConnections.cjs +125 -0
  9. package/dist/cjs/FrappeFormConnections.native.js +149 -0
  10. package/dist/cjs/FrappeFormConnections.native.js.map +1 -0
  11. package/dist/cjs/FrappeImageView.cjs +7 -9
  12. package/dist/cjs/FrappeImageView.native.js +7 -13
  13. package/dist/cjs/FrappeImageView.native.js.map +1 -1
  14. package/dist/cjs/FrappeList.cjs +29 -4
  15. package/dist/cjs/FrappeList.native.js +37 -5
  16. package/dist/cjs/FrappeList.native.js.map +1 -1
  17. package/dist/cjs/FrappeListView.cjs +102 -29
  18. package/dist/cjs/FrappeListView.native.js +109 -29
  19. package/dist/cjs/FrappeListView.native.js.map +1 -1
  20. package/dist/cjs/FrappeProvider.cjs +7 -2
  21. package/dist/cjs/FrappeProvider.native.js +7 -2
  22. package/dist/cjs/FrappeProvider.native.js.map +1 -1
  23. package/dist/cjs/FrappeReportBuilder.cjs +10 -11
  24. package/dist/cjs/FrappeReportBuilder.native.js +10 -16
  25. package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
  26. package/dist/cjs/FrappeRowsReader.cjs +49 -0
  27. package/dist/cjs/FrappeRowsReader.native.js +53 -0
  28. package/dist/cjs/FrappeRowsReader.native.js.map +1 -0
  29. package/dist/cjs/FrappeTable.cjs +18 -6
  30. package/dist/cjs/FrappeTable.native.js +24 -6
  31. package/dist/cjs/FrappeTable.native.js.map +1 -1
  32. package/dist/cjs/fieldRegistry.cjs +36 -11
  33. package/dist/cjs/fieldRegistry.native.js +59 -11
  34. package/dist/cjs/fieldRegistry.native.js.map +1 -1
  35. package/dist/cjs/fields/Date/index.native.js +5 -2
  36. package/dist/cjs/fields/Date/index.native.js.map +1 -1
  37. package/dist/cjs/fields/Date/sheetKeyboard.cjs +2 -2
  38. package/dist/cjs/fields/Date/sheetKeyboard.native.js +2 -2
  39. package/dist/cjs/fields/Date/sheetKeyboard.native.js.map +1 -1
  40. package/dist/cjs/fields/DateRange/index.cjs +2 -1
  41. package/dist/cjs/fields/DateRange/index.native.js +4 -1
  42. package/dist/cjs/fields/DateRange/index.native.js.map +1 -1
  43. package/dist/cjs/formEngine/index.cjs +4 -2
  44. package/dist/cjs/formEngine/index.native.js +3 -1
  45. package/dist/cjs/formEngine/index.native.js.map +1 -1
  46. package/dist/cjs/formEngine/useSessionRoles.cjs +77 -0
  47. package/dist/cjs/formEngine/useSessionRoles.native.js +84 -0
  48. package/dist/cjs/formEngine/useSessionRoles.native.js.map +1 -0
  49. package/dist/cjs/frappeConnections.cjs +41 -0
  50. package/dist/cjs/frappeConnections.native.js +50 -0
  51. package/dist/cjs/frappeConnections.native.js.map +1 -0
  52. package/dist/cjs/frappeNavigation.cjs +107 -0
  53. package/dist/cjs/frappeNavigation.native.js +112 -0
  54. package/dist/cjs/frappeNavigation.native.js.map +1 -0
  55. package/dist/cjs/index.cjs +18 -1
  56. package/dist/cjs/index.native.js +18 -1
  57. package/dist/cjs/index.native.js.map +1 -1
  58. package/dist/cjs/schemaForm/index.cjs +4 -1
  59. package/dist/cjs/schemaForm/index.native.js +4 -1
  60. package/dist/cjs/schemaForm/index.native.js.map +1 -1
  61. package/dist/cjs/useConnectionFilters.cjs +146 -0
  62. package/dist/cjs/useConnectionFilters.native.js +213 -0
  63. package/dist/cjs/useConnectionFilters.native.js.map +1 -0
  64. package/dist/cjs/useFrappePagination.cjs +37 -18
  65. package/dist/cjs/useFrappePagination.native.js +59 -23
  66. package/dist/cjs/useFrappePagination.native.js.map +1 -1
  67. package/dist/cjs/useFrappeRows.native.js.map +1 -1
  68. package/dist/esm/FrappeDashboard.mjs +9 -11
  69. package/dist/esm/FrappeDashboard.mjs.map +1 -1
  70. package/dist/esm/FrappeDashboard.native.js +9 -15
  71. package/dist/esm/FrappeDashboard.native.js.map +1 -1
  72. package/dist/esm/FrappeForm.mjs +471 -181
  73. package/dist/esm/FrappeForm.mjs.map +1 -1
  74. package/dist/esm/FrappeForm.native.js +599 -195
  75. package/dist/esm/FrappeForm.native.js.map +1 -1
  76. package/dist/esm/FrappeFormConnections.mjs +99 -0
  77. package/dist/esm/FrappeFormConnections.mjs.map +1 -0
  78. package/dist/esm/FrappeFormConnections.native.js +120 -0
  79. package/dist/esm/FrappeFormConnections.native.js.map +1 -0
  80. package/dist/esm/FrappeImageView.mjs +8 -10
  81. package/dist/esm/FrappeImageView.mjs.map +1 -1
  82. package/dist/esm/FrappeImageView.native.js +8 -14
  83. package/dist/esm/FrappeImageView.native.js.map +1 -1
  84. package/dist/esm/FrappeList.mjs +31 -6
  85. package/dist/esm/FrappeList.mjs.map +1 -1
  86. package/dist/esm/FrappeList.native.js +39 -7
  87. package/dist/esm/FrappeList.native.js.map +1 -1
  88. package/dist/esm/FrappeListView.mjs +103 -30
  89. package/dist/esm/FrappeListView.mjs.map +1 -1
  90. package/dist/esm/FrappeListView.native.js +110 -30
  91. package/dist/esm/FrappeListView.native.js.map +1 -1
  92. package/dist/esm/FrappeProvider.mjs +7 -2
  93. package/dist/esm/FrappeProvider.mjs.map +1 -1
  94. package/dist/esm/FrappeProvider.native.js +7 -2
  95. package/dist/esm/FrappeProvider.native.js.map +1 -1
  96. package/dist/esm/FrappeReportBuilder.mjs +11 -12
  97. package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
  98. package/dist/esm/FrappeReportBuilder.native.js +11 -17
  99. package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
  100. package/dist/esm/FrappeRowsReader.mjs +24 -0
  101. package/dist/esm/FrappeRowsReader.mjs.map +1 -0
  102. package/dist/esm/FrappeRowsReader.native.js +25 -0
  103. package/dist/esm/FrappeRowsReader.native.js.map +1 -0
  104. package/dist/esm/FrappeTable.mjs +18 -6
  105. package/dist/esm/FrappeTable.mjs.map +1 -1
  106. package/dist/esm/FrappeTable.native.js +24 -6
  107. package/dist/esm/FrappeTable.native.js.map +1 -1
  108. package/dist/esm/fieldRegistry.mjs +35 -12
  109. package/dist/esm/fieldRegistry.mjs.map +1 -1
  110. package/dist/esm/fieldRegistry.native.js +58 -12
  111. package/dist/esm/fieldRegistry.native.js.map +1 -1
  112. package/dist/esm/fields/Date/index.native.js +6 -3
  113. package/dist/esm/fields/Date/index.native.js.map +1 -1
  114. package/dist/esm/fields/Date/sheetKeyboard.mjs +2 -2
  115. package/dist/esm/fields/Date/sheetKeyboard.mjs.map +1 -1
  116. package/dist/esm/fields/Date/sheetKeyboard.native.js +2 -2
  117. package/dist/esm/fields/Date/sheetKeyboard.native.js.map +1 -1
  118. package/dist/esm/fields/DateRange/index.mjs +2 -1
  119. package/dist/esm/fields/DateRange/index.mjs.map +1 -1
  120. package/dist/esm/fields/DateRange/index.native.js +4 -1
  121. package/dist/esm/fields/DateRange/index.native.js.map +1 -1
  122. package/dist/esm/formEngine/index.mjs +2 -1
  123. package/dist/esm/formEngine/index.mjs.map +1 -1
  124. package/dist/esm/formEngine/index.native.js +2 -1
  125. package/dist/esm/formEngine/index.native.js.map +1 -1
  126. package/dist/esm/formEngine/useSessionRoles.mjs +52 -0
  127. package/dist/esm/formEngine/useSessionRoles.mjs.map +1 -0
  128. package/dist/esm/formEngine/useSessionRoles.native.js +56 -0
  129. package/dist/esm/formEngine/useSessionRoles.native.js.map +1 -0
  130. package/dist/esm/frappeConnections.mjs +15 -0
  131. package/dist/esm/frappeConnections.mjs.map +1 -0
  132. package/dist/esm/frappeConnections.native.js +21 -0
  133. package/dist/esm/frappeConnections.native.js.map +1 -0
  134. package/dist/esm/frappeNavigation.mjs +78 -0
  135. package/dist/esm/frappeNavigation.mjs.map +1 -0
  136. package/dist/esm/frappeNavigation.native.js +80 -0
  137. package/dist/esm/frappeNavigation.native.js.map +1 -0
  138. package/dist/esm/index.mjs +7 -3
  139. package/dist/esm/index.mjs.map +1 -1
  140. package/dist/esm/index.native.js +7 -3
  141. package/dist/esm/index.native.js.map +1 -1
  142. package/dist/esm/schemaForm/index.mjs +4 -1
  143. package/dist/esm/schemaForm/index.mjs.map +1 -1
  144. package/dist/esm/schemaForm/index.native.js +4 -1
  145. package/dist/esm/schemaForm/index.native.js.map +1 -1
  146. package/dist/esm/useConnectionFilters.mjs +121 -0
  147. package/dist/esm/useConnectionFilters.mjs.map +1 -0
  148. package/dist/esm/useConnectionFilters.native.js +185 -0
  149. package/dist/esm/useConnectionFilters.native.js.map +1 -0
  150. package/dist/esm/useFrappePagination.mjs +38 -19
  151. package/dist/esm/useFrappePagination.mjs.map +1 -1
  152. package/dist/esm/useFrappePagination.native.js +60 -24
  153. package/dist/esm/useFrappePagination.native.js.map +1 -1
  154. package/dist/esm/useFrappeRows.mjs.map +1 -1
  155. package/dist/esm/useFrappeRows.native.js.map +1 -1
  156. package/dist/jsx/FrappeDashboard.mjs +9 -11
  157. package/dist/jsx/FrappeDashboard.mjs.map +1 -1
  158. package/dist/jsx/FrappeDashboard.native.js +8 -14
  159. package/dist/jsx/FrappeDashboard.native.js.map +1 -1
  160. package/dist/jsx/FrappeForm.mjs +471 -181
  161. package/dist/jsx/FrappeForm.mjs.map +1 -1
  162. package/dist/jsx/FrappeForm.native.js +611 -207
  163. package/dist/jsx/FrappeForm.native.js.map +1 -1
  164. package/dist/jsx/FrappeFormConnections.mjs +99 -0
  165. package/dist/jsx/FrappeFormConnections.mjs.map +1 -0
  166. package/dist/jsx/FrappeFormConnections.native.js +149 -0
  167. package/dist/jsx/FrappeFormConnections.native.js.map +1 -0
  168. package/dist/jsx/FrappeImageView.mjs +8 -10
  169. package/dist/jsx/FrappeImageView.mjs.map +1 -1
  170. package/dist/jsx/FrappeImageView.native.js +7 -13
  171. package/dist/jsx/FrappeImageView.native.js.map +1 -1
  172. package/dist/jsx/FrappeList.mjs +31 -6
  173. package/dist/jsx/FrappeList.mjs.map +1 -1
  174. package/dist/jsx/FrappeList.native.js +37 -5
  175. package/dist/jsx/FrappeList.native.js.map +1 -1
  176. package/dist/jsx/FrappeListView.mjs +103 -30
  177. package/dist/jsx/FrappeListView.mjs.map +1 -1
  178. package/dist/jsx/FrappeListView.native.js +109 -29
  179. package/dist/jsx/FrappeListView.native.js.map +1 -1
  180. package/dist/jsx/FrappeProvider.mjs +7 -2
  181. package/dist/jsx/FrappeProvider.mjs.map +1 -1
  182. package/dist/jsx/FrappeProvider.native.js +7 -2
  183. package/dist/jsx/FrappeProvider.native.js.map +1 -1
  184. package/dist/jsx/FrappeReportBuilder.mjs +11 -12
  185. package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
  186. package/dist/jsx/FrappeReportBuilder.native.js +10 -16
  187. package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
  188. package/dist/jsx/FrappeRowsReader.mjs +24 -0
  189. package/dist/jsx/FrappeRowsReader.mjs.map +1 -0
  190. package/dist/jsx/FrappeRowsReader.native.js +53 -0
  191. package/dist/jsx/FrappeRowsReader.native.js.map +1 -0
  192. package/dist/jsx/FrappeTable.mjs +18 -6
  193. package/dist/jsx/FrappeTable.mjs.map +1 -1
  194. package/dist/jsx/FrappeTable.native.js +24 -6
  195. package/dist/jsx/FrappeTable.native.js.map +1 -1
  196. package/dist/jsx/fieldRegistry.mjs +35 -12
  197. package/dist/jsx/fieldRegistry.mjs.map +1 -1
  198. package/dist/jsx/fieldRegistry.native.js +59 -11
  199. package/dist/jsx/fieldRegistry.native.js.map +1 -1
  200. package/dist/jsx/fields/Date/index.native.js +5 -2
  201. package/dist/jsx/fields/Date/index.native.js.map +1 -1
  202. package/dist/jsx/fields/Date/sheetKeyboard.mjs +2 -2
  203. package/dist/jsx/fields/Date/sheetKeyboard.mjs.map +1 -1
  204. package/dist/jsx/fields/Date/sheetKeyboard.native.js +2 -2
  205. package/dist/jsx/fields/Date/sheetKeyboard.native.js.map +1 -1
  206. package/dist/jsx/fields/DateRange/index.mjs +2 -1
  207. package/dist/jsx/fields/DateRange/index.mjs.map +1 -1
  208. package/dist/jsx/fields/DateRange/index.native.js +4 -1
  209. package/dist/jsx/fields/DateRange/index.native.js.map +1 -1
  210. package/dist/jsx/formEngine/index.mjs +2 -1
  211. package/dist/jsx/formEngine/index.mjs.map +1 -1
  212. package/dist/jsx/formEngine/index.native.js +3 -1
  213. package/dist/jsx/formEngine/index.native.js.map +1 -1
  214. package/dist/jsx/formEngine/useSessionRoles.mjs +52 -0
  215. package/dist/jsx/formEngine/useSessionRoles.mjs.map +1 -0
  216. package/dist/jsx/formEngine/useSessionRoles.native.js +84 -0
  217. package/dist/jsx/formEngine/useSessionRoles.native.js.map +1 -0
  218. package/dist/jsx/frappeConnections.mjs +15 -0
  219. package/dist/jsx/frappeConnections.mjs.map +1 -0
  220. package/dist/jsx/frappeConnections.native.js +50 -0
  221. package/dist/jsx/frappeConnections.native.js.map +1 -0
  222. package/dist/jsx/frappeNavigation.mjs +78 -0
  223. package/dist/jsx/frappeNavigation.mjs.map +1 -0
  224. package/dist/jsx/frappeNavigation.native.js +112 -0
  225. package/dist/jsx/frappeNavigation.native.js.map +1 -0
  226. package/dist/jsx/index.js +7 -3
  227. package/dist/jsx/index.js.map +1 -1
  228. package/dist/jsx/index.mjs +7 -3
  229. package/dist/jsx/index.mjs.map +1 -1
  230. package/dist/jsx/index.native.js +18 -1
  231. package/dist/jsx/index.native.js.map +1 -1
  232. package/dist/jsx/schemaForm/index.mjs +4 -1
  233. package/dist/jsx/schemaForm/index.mjs.map +1 -1
  234. package/dist/jsx/schemaForm/index.native.js +4 -1
  235. package/dist/jsx/schemaForm/index.native.js.map +1 -1
  236. package/dist/jsx/useConnectionFilters.mjs +121 -0
  237. package/dist/jsx/useConnectionFilters.mjs.map +1 -0
  238. package/dist/jsx/useConnectionFilters.native.js +213 -0
  239. package/dist/jsx/useConnectionFilters.native.js.map +1 -0
  240. package/dist/jsx/useFrappePagination.mjs +38 -19
  241. package/dist/jsx/useFrappePagination.mjs.map +1 -1
  242. package/dist/jsx/useFrappePagination.native.js +59 -23
  243. package/dist/jsx/useFrappePagination.native.js.map +1 -1
  244. package/dist/jsx/useFrappeRows.mjs.map +1 -1
  245. package/dist/jsx/useFrappeRows.native.js.map +1 -1
  246. package/package.json +24 -20
  247. package/src/FileBrowser/AttachLibrary.stories.tsx +23 -0
  248. package/src/FrappeDashboard.tsx +11 -16
  249. package/src/FrappeForm.connections.spec.tsx +280 -0
  250. package/src/FrappeForm.connections.stories.tsx +93 -0
  251. package/src/FrappeForm.deskSemantics.spec.tsx +284 -0
  252. package/src/FrappeForm.tsx +560 -145
  253. package/src/FrappeFormConnections.tsx +148 -0
  254. package/src/FrappeGuestMeta.spec.tsx +143 -0
  255. package/src/FrappeImageView.tsx +8 -12
  256. package/src/FrappeList.tsx +71 -15
  257. package/src/FrappeListView.cards.spec.tsx +175 -0
  258. package/src/FrappeListView.cards.stories.tsx +131 -0
  259. package/src/FrappeListView.connections.spec.tsx +192 -0
  260. package/src/FrappeListView.connections.stories.tsx +99 -0
  261. package/src/FrappeListView.seed.spec.tsx +228 -0
  262. package/src/FrappeListView.tsx +177 -31
  263. package/src/FrappeProvider.tsx +13 -1
  264. package/src/FrappeReportBuilder.tsx +11 -19
  265. package/src/FrappeRowsReader.tsx +38 -0
  266. package/src/FrappeTable.optionalProps.spec.tsx +249 -0
  267. package/src/FrappeTable.spec.tsx +32 -42
  268. package/src/FrappeTable.stories.spec.tsx +55 -0
  269. package/src/FrappeTable.stories.tsx +28 -37
  270. package/src/FrappeTable.tsx +108 -45
  271. package/src/childTableDependsOn.spec.tsx +85 -0
  272. package/src/desk/DeskFormSidebar.spec.tsx +20 -0
  273. package/src/desk/DeskFormSidebar.stories.tsx +42 -0
  274. package/src/fieldRegistry.spec.tsx +27 -64
  275. package/src/fieldRegistry.ts +71 -14
  276. package/src/fields/Date/index.native.tsx +4 -2
  277. package/src/fields/Date/sheetKeyboard.spec.ts +14 -5
  278. package/src/fields/Date/sheetKeyboard.ts +12 -4
  279. package/src/fields/DateRange/DateRange.spec.tsx +67 -0
  280. package/src/fields/DateRange/index.tsx +5 -1
  281. package/src/fields/LinkTree.spec.tsx +8 -0
  282. package/src/fields/LinkTree.stories.tsx +28 -5
  283. package/src/fields/Select/Select.hydration.spec.tsx +177 -0
  284. package/src/formEngine/doctypeMeta.ts +15 -114
  285. package/src/formEngine/index.ts +2 -0
  286. package/src/formEngine/seam.spec.ts +23 -4
  287. package/src/formEngine/useSessionRoles.ts +73 -0
  288. package/src/frappeConnections.ts +50 -0
  289. package/src/frappeNavigation.tsx +112 -0
  290. package/src/frappeRoutes.spec.tsx +335 -0
  291. package/src/index.ts +28 -1
  292. package/src/schemaForm/index.tsx +1 -0
  293. package/src/useConnectionFilters.tsx +157 -0
  294. package/src/useFrappePagination.ts +70 -26
  295. package/src/useFrappeRows.sharedPath.spec.tsx +206 -0
  296. package/src/useFrappeRows.ts +5 -4
  297. package/types/FrappeDashboard.d.ts +3 -4
  298. package/types/FrappeDashboard.d.ts.map +1 -1
  299. package/types/FrappeForm.d.ts +57 -5
  300. package/types/FrappeForm.d.ts.map +1 -1
  301. package/types/FrappeFormConnections.d.ts +18 -0
  302. package/types/FrappeFormConnections.d.ts.map +1 -0
  303. package/types/FrappeImageView.d.ts +2 -3
  304. package/types/FrappeImageView.d.ts.map +1 -1
  305. package/types/FrappeList.d.ts +20 -2
  306. package/types/FrappeList.d.ts.map +1 -1
  307. package/types/FrappeListView.d.ts +53 -3
  308. package/types/FrappeListView.d.ts.map +1 -1
  309. package/types/FrappeProvider.d.ts +8 -1
  310. package/types/FrappeProvider.d.ts.map +1 -1
  311. package/types/FrappeReportBuilder.d.ts.map +1 -1
  312. package/types/FrappeRowsReader.d.ts +26 -0
  313. package/types/FrappeRowsReader.d.ts.map +1 -0
  314. package/types/FrappeTable.d.ts +39 -8
  315. package/types/FrappeTable.d.ts.map +1 -1
  316. package/types/fieldRegistry.d.ts +17 -3
  317. package/types/fieldRegistry.d.ts.map +1 -1
  318. package/types/fields/Date/index.native.d.ts.map +1 -1
  319. package/types/fields/Date/sheetKeyboard.d.ts +4 -3
  320. package/types/fields/Date/sheetKeyboard.d.ts.map +1 -1
  321. package/types/fields/DateRange/index.d.ts.map +1 -1
  322. package/types/formEngine/doctypeMeta.d.ts +10 -114
  323. package/types/formEngine/doctypeMeta.d.ts.map +1 -1
  324. package/types/formEngine/index.d.ts +1 -0
  325. package/types/formEngine/index.d.ts.map +1 -1
  326. package/types/formEngine/useSessionRoles.d.ts +19 -0
  327. package/types/formEngine/useSessionRoles.d.ts.map +1 -0
  328. package/types/frappeConnections.d.ts +33 -0
  329. package/types/frappeConnections.d.ts.map +1 -0
  330. package/types/frappeNavigation.d.ts +49 -0
  331. package/types/frappeNavigation.d.ts.map +1 -0
  332. package/types/index.d.ts +7 -3
  333. package/types/index.d.ts.map +1 -1
  334. package/types/schemaForm/index.d.ts.map +1 -1
  335. package/types/useConnectionFilters.d.ts +34 -0
  336. package/types/useConnectionFilters.d.ts.map +1 -0
  337. package/types/useFrappePagination.d.ts +18 -5
  338. package/types/useFrappePagination.d.ts.map +1 -1
  339. package/types/useFrappeRows.d.ts +5 -4
  340. package/types/useFrappeRows.d.ts.map +1 -1
  341. package/src/FileBrowser/AttachLibrary.spec.tsx +0 -74
@@ -16,16 +16,25 @@ import {
16
16
 
17
17
  describe("dateSheetMaxHeight", () => {
18
18
  it("uses the 85% detent when the keyboard is down", () => {
19
- expect(dateSheetMaxHeight(800, 0)).toBe(Math.round(800 * 0.85));
19
+ expect(dateSheetMaxHeight(800, 0, 47)).toBe(Math.round(800 * 0.85));
20
20
  });
21
21
 
22
22
  it("caps to the space above the keyboard, less the 48pt backdrop strip", () => {
23
- expect(dateSheetMaxHeight(800, 400)).toBe(800 - 400 - DATE_SHEET_BACKDROP_STRIP);
24
- expect(dateSheetMaxHeight(800, 400)).toBeLessThan(dateSheetMaxHeight(800, 0));
23
+ expect(dateSheetMaxHeight(800, 400, 0)).toBe(800 - 400 - DATE_SHEET_BACKDROP_STRIP);
24
+ expect(dateSheetMaxHeight(800, 400, 0)).toBeLessThan(dateSheetMaxHeight(800, 0, 0));
25
+ });
26
+
27
+ it("keeps the backdrop strip below the status bar, not inside it", () => {
28
+ const windowHeight = 874;
29
+ const keyboardInset = 335;
30
+ const topInset = 62;
31
+ const sheetTop =
32
+ windowHeight - keyboardInset - dateSheetMaxHeight(windowHeight, keyboardInset, topInset);
33
+ expect(sheetTop - topInset).toBe(DATE_SHEET_BACKDROP_STRIP);
25
34
  });
26
35
 
27
36
  it("never collapses below the minimum sheet height", () => {
28
- expect(dateSheetMaxHeight(800, 700)).toBe(DATE_SHEET_MIN_HEIGHT);
37
+ expect(dateSheetMaxHeight(800, 700, 47)).toBe(DATE_SHEET_MIN_HEIGHT);
29
38
  });
30
39
  });
31
40
 
@@ -35,7 +44,7 @@ describe("datePickerHeight", () => {
35
44
  });
36
45
 
37
46
  it("shrinks the calendar so the sheet can sit above the keyboard", () => {
38
- const maxHeight = dateSheetMaxHeight(800, 400);
47
+ const maxHeight = dateSheetMaxHeight(800, 400, 47);
39
48
  const height = datePickerHeight("date", maxHeight);
40
49
  expect(height).toBeLessThan(DATE_PICKER_CALENDAR_HEIGHT);
41
50
  expect(height).toBeLessThanOrEqual(maxHeight);
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * The Date iOS sheet used to give the calendar a fixed 360pt height inside a
5
5
  * Modal. KeyboardAvoidingView cannot shrink that shape — it pads the container
6
- * and the sheet overflows off the top. Cap the sheet to the space ABOVE the
7
- * keyboard (less a strip of backdrop) and shrink the picker to fit.
6
+ * and the sheet overflows off the top. Cap the sheet to the space between the
7
+ * status bar and the keyboard (less a strip of backdrop) and shrink the picker
8
+ * to fit.
8
9
  */
9
10
 
10
11
  export const DATE_SHEET_MAX_HEIGHT_RATIO = 0.85;
@@ -15,12 +16,19 @@ export const DATE_PICKER_CALENDAR_HEIGHT = 360;
15
16
  export const DATE_PICKER_WHEEL_HEIGHT = 200;
16
17
  const minPickerHeight = 120;
17
18
 
18
- export function dateSheetMaxHeight(windowHeight: number, keyboardInset: number): number {
19
+ export function dateSheetMaxHeight(
20
+ windowHeight: number,
21
+ keyboardInset: number,
22
+ topInset: number,
23
+ ): number {
19
24
  const available = Math.max(0, windowHeight - keyboardInset);
20
25
  return Math.max(
21
26
  DATE_SHEET_MIN_HEIGHT,
22
27
  Math.round(
23
- Math.min(windowHeight * DATE_SHEET_MAX_HEIGHT_RATIO, available - DATE_SHEET_BACKDROP_STRIP),
28
+ Math.min(
29
+ windowHeight * DATE_SHEET_MAX_HEIGHT_RATIO,
30
+ available - topInset - DATE_SHEET_BACKDROP_STRIP,
31
+ ),
24
32
  ),
25
33
  );
26
34
  }
@@ -8,7 +8,9 @@
8
8
  * spec pass). Both are now wired, and the range is carried as the two calendar
9
9
  * dates comma-separated — the shape the registry feeds in and reads back.
10
10
  */
11
+ import { Form } from "@multiplatform.one/forms";
11
12
  import { renderWithProviders } from "@multiplatform.one/test-utils";
13
+ import { useForm } from "@tanstack/react-form";
12
14
  import { act, cleanup, fireEvent, waitFor } from "@testing-library/react";
13
15
  import type React from "react";
14
16
  import { afterEach, describe, expect, it, vi } from "vitest";
@@ -181,6 +183,71 @@ describe("DateRangeField controlled value (MPO-137)", () => {
181
183
  });
182
184
  });
183
185
 
186
+ describe("DateRangeField uncontrolled and form-bound paths (MPO-137)", () => {
187
+ it("seeds an uncontrolled field from a wire defaultValue and still emits a pick", async () => {
188
+ const onChange = vi.fn();
189
+ const result = renderWithProviders(
190
+ <DateRangeField label="Period" defaultValue="2026-03-05,2026-03-31" onChange={onChange} />,
191
+ );
192
+ expect(trigger(result.container)?.value).toBe("Mar 5, 2026 – Mar 31, 2026");
193
+
194
+ await act(async () => {
195
+ fireEvent.click(trigger(result.container)!);
196
+ });
197
+ await waitFor(() => {
198
+ expect(getDayButton(15)).toBeTruthy();
199
+ });
200
+ await act(async () => {
201
+ fireEvent.click(getDayButton(15)!);
202
+ });
203
+ await act(async () => {
204
+ fireEvent.click(getDayButton(20)!);
205
+ });
206
+ expect(onChange).toHaveBeenCalledTimes(1);
207
+ expect(onChange).toHaveBeenCalledWith("2026-03-15,2026-03-20");
208
+ expect(trigger(result.container)?.value).toBe("Mar 15, 2026 – Mar 20, 2026");
209
+ });
210
+
211
+ it("binds name/form inside a TanStack form: the form value paints and a pick writes back", async () => {
212
+ type Range = { start: Date | null; end: Date | null } | null;
213
+ let formApi: { state: { values: { period: Range } } } | undefined;
214
+ function Harness() {
215
+ const form = useForm({
216
+ defaultValues: {
217
+ period: { start: new Date(2026, 2, 5), end: new Date(2026, 2, 31) } as Range,
218
+ },
219
+ });
220
+ formApi = form as unknown as typeof formApi;
221
+ return (
222
+ <Form form={form}>
223
+ <DateRangeField label="Period" name="period" form={form} />
224
+ </Form>
225
+ );
226
+ }
227
+ const result = renderWithProviders(<Harness />);
228
+ await waitFor(() => {
229
+ expect(trigger(result.container)?.value).toBe("Mar 5, 2026 – Mar 31, 2026");
230
+ });
231
+
232
+ await act(async () => {
233
+ fireEvent.click(trigger(result.container)!);
234
+ });
235
+ await waitFor(() => {
236
+ expect(getDayButton(15)).toBeTruthy();
237
+ });
238
+ await act(async () => {
239
+ fireEvent.click(getDayButton(15)!);
240
+ });
241
+ await act(async () => {
242
+ fireEvent.click(getDayButton(20)!);
243
+ });
244
+ const period = formApi?.state.values.period;
245
+ expect(period?.start?.toDateString()).toBe(new Date(2026, 2, 15).toDateString());
246
+ expect(period?.end?.toDateString()).toBe(new Date(2026, 2, 20).toDateString());
247
+ expect(trigger(result.container)?.value).toBe("Mar 15, 2026 – Mar 20, 2026");
248
+ });
249
+ });
250
+
184
251
  describe("registry resolution (Date Range fieldtype)", () => {
185
252
  function propsFor(overrides: Partial<FieldRenderProps> = {}): FieldRenderProps {
186
253
  return { value: "", onChange: () => {}, ...overrides };
@@ -97,6 +97,10 @@ export function DateRangeField<
97
97
  // re-seeds its display state from every new `value` identity, which would
98
98
  // otherwise discard a half-picked range on an unrelated re-render.
99
99
  const rangeValue = useMemo(() => toPickerRange(wireValue), [wireValue]);
100
+ const rangeDefault = useMemo(
101
+ () => (typeof defaultValue === "string" ? toPickerRange(defaultValue) : defaultValue),
102
+ [defaultValue],
103
+ );
100
104
 
101
105
  return (
102
106
  <FormsDateRangePicker
@@ -105,7 +109,7 @@ export function DateRangeField<
105
109
  validators={validators}
106
110
  value={rangeValue}
107
111
  onChange={(range) => emit?.(toWireValue(range))}
108
- defaultValue={defaultValue}
112
+ defaultValue={rangeDefault}
109
113
  label={fieldProps.label}
110
114
  labelProps={fieldProps.labelProps}
111
115
  error={fieldProps.error}
@@ -101,6 +101,14 @@ describe("LinkTree adapter (MPO-111)", () => {
101
101
  expect(screen.queryByText("Should not win")).toBeNull();
102
102
  });
103
103
 
104
+ it("still mounts TreeSelect with the placeholder when the forest is empty", () => {
105
+ mockUseLiveQuery.mockReturnValue({ data: [], isLoading: false, isError: false });
106
+ const result = renderTree();
107
+ expect(result.container.querySelector('[data-testid="tree-select"]')).toBeTruthy();
108
+ expect(result.container.querySelector('[data-testid="tree-select-trigger"]')).toBeTruthy();
109
+ expect(screen.getByText("Select Account")).toBeTruthy();
110
+ });
111
+
104
112
  it("does not call onChange when disabled", () => {
105
113
  const onChange = vi.fn();
106
114
  renderTree({ value: "Assets", onChange, disabled: true });
@@ -114,12 +114,9 @@ function regionMeta(name: string) {
114
114
  };
115
115
  })();
116
116
 
117
- function host(children: ReactNode) {
117
+ function host(children: ReactNode, docs: Region[] = regionDocs) {
118
118
  return (
119
- <FrappeProvider
120
- baseURL={MOCK_HOST}
121
- fixtures={new InMemoryFixtureProvider({ QaRegion: regionDocs })}
122
- >
119
+ <FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({ QaRegion: docs })}>
123
120
  {children}
124
121
  </FrappeProvider>
125
122
  );
@@ -160,6 +157,32 @@ export const AllStates = () =>
160
157
  </YStack>,
161
158
  );
162
159
 
160
+ export const Empty = () => host(<ControlledTree label="Region (empty forest)" />, []);
161
+ Empty.parameters = {
162
+ docs: {
163
+ description: {
164
+ story:
165
+ "Tree DocType with no rows: TreeSelect still mounts with the placeholder and an empty forest, so the field never degrades to a blank or a spinner that never settles.",
166
+ },
167
+ },
168
+ };
169
+
170
+ export const Nested = () =>
171
+ host(
172
+ <YStack gap="$4" width={400}>
173
+ <ControlledTree label="Leaf, three levels deep" value="North A1" />
174
+ <ControlledTree label="Branch, two levels deep" value="North A" />
175
+ </YStack>,
176
+ );
177
+ Nested.parameters = {
178
+ docs: {
179
+ description: {
180
+ story:
181
+ "North > North A > North A1: a stored link to a nested node resolves through the loaded forest, so the trigger shows the node's title_field, not its name.",
182
+ },
183
+ },
184
+ };
185
+
163
186
  export const ViaLink = () => {
164
187
  const [value, setValue] = useState("North A");
165
188
  return host(
@@ -0,0 +1,177 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * MPO-279: the Frappe `Select` field (the registry's SelectField) server-renders
4
+ * the sheet trigger at every width, because the server has no viewport. These
5
+ * hydrate that markup at a phone width and a desktop width, then drive the
6
+ * hydrated control by pointer and by keyboard and read its combobox ARIA.
7
+ */
8
+ import { TestProviders } from "@multiplatform.one/test-utils";
9
+ import { fireEvent, waitFor } from "@testing-library/react";
10
+ import { act, useLayoutEffect, useState } from "react";
11
+ import { hydrateRoot, type Root } from "react-dom/client";
12
+ import { renderToString } from "react-dom/server";
13
+ import { afterEach, describe, expect, it, vi } from "vitest";
14
+ import { Select } from ".";
15
+
16
+ const options = ["Alpha", "Beta", "Gamma", "Delta"];
17
+ let onChange = vi.fn();
18
+ let hydrationTrigger: Element | null = null;
19
+
20
+ function CommitProbe() {
21
+ useLayoutEffect(() => {
22
+ hydrationTrigger ??= document.querySelector('[role="combobox"]');
23
+ }, []);
24
+ return null;
25
+ }
26
+
27
+ function Fixture() {
28
+ const [value, setValue] = useState("Beta");
29
+ return (
30
+ <TestProviders>
31
+ <output>{value}</output>
32
+ <Select
33
+ label="Choice"
34
+ options={options}
35
+ value={value}
36
+ onChange={(next: unknown) => {
37
+ onChange(next);
38
+ setValue(String(next));
39
+ }}
40
+ />
41
+ <CommitProbe />
42
+ </TestProviders>
43
+ );
44
+ }
45
+
46
+ let root: Root | undefined;
47
+ let container: HTMLDivElement | undefined;
48
+ let errors: unknown[] = [];
49
+
50
+ async function hydrateAt(width: number) {
51
+ vi.spyOn(window, "scrollTo").mockImplementation(() => {});
52
+ Object.defineProperty(window, "innerWidth", { configurable: true, value: width, writable: true });
53
+ const browserWindow = window;
54
+ let html: string;
55
+ vi.stubGlobal("window", undefined);
56
+ try {
57
+ html = renderToString(<Fixture />);
58
+ } finally {
59
+ vi.stubGlobal("window", browserWindow);
60
+ }
61
+ container = document.createElement("div");
62
+ container.innerHTML = html;
63
+ document.body.append(container);
64
+ const serverTrigger = container.querySelector('[role="combobox"]');
65
+ errors = [];
66
+ hydrationTrigger = null;
67
+ onChange = vi.fn();
68
+ vi.spyOn(console, "error").mockImplementation((...args) => errors.push(args));
69
+ await act(async () => {
70
+ root = hydrateRoot(container!, <Fixture />, {
71
+ onRecoverableError: (error) => errors.push(error),
72
+ });
73
+ });
74
+ return { serverTrigger, view: container };
75
+ }
76
+
77
+ const hydrationErrors = () =>
78
+ errors.filter((error) => /hydration|hydrated|didn't match/i.test(String(error)));
79
+ const trigger = () => document.querySelector('[role="combobox"]') as HTMLElement;
80
+ const listOptions = () => [...document.querySelectorAll('[role="option"]')] as HTMLElement[];
81
+ const option = (label: string) => listOptions().find((node) => node.textContent?.includes(label))!;
82
+
83
+ /** One act per event: a browser flushes React between discrete events. */
84
+ async function press(node: Element) {
85
+ await act(async () => fireEvent.pointerDown(node, { button: 0, pointerId: 1 }));
86
+ await act(async () => fireEvent.mouseDown(node, { detail: 1 }));
87
+ await act(async () => fireEvent.mouseUp(node, { detail: 1 }));
88
+ await act(async () => fireEvent.click(node, { detail: 1 }));
89
+ }
90
+
91
+ const focused = () => document.activeElement as HTMLElement;
92
+
93
+ async function key(node: Element, name: string) {
94
+ await act(async () => {
95
+ fireEvent.keyDown(node, { key: name, code: name === " " ? "Space" : name });
96
+ });
97
+ }
98
+
99
+ afterEach(async () => {
100
+ if (root) await act(async () => root?.unmount());
101
+ root = undefined;
102
+ container?.remove();
103
+ container = undefined;
104
+ vi.restoreAllMocks();
105
+ vi.unstubAllGlobals();
106
+ });
107
+
108
+ describe("SelectField SSR to hydration (MPO-279)", () => {
109
+ it.each([390, 1200])(
110
+ "hydrates the server trigger at %i pixels with no recoverable error",
111
+ async (width) => {
112
+ const { serverTrigger } = await hydrateAt(width);
113
+ expect(serverTrigger).toBeTruthy();
114
+ expect(hydrationTrigger).toBe(serverTrigger);
115
+ expect(hydrationErrors()).toEqual([]);
116
+ await waitFor(() => expect(trigger().textContent).toContain("Beta"));
117
+ expect(trigger().getAttribute("aria-haspopup")).toBe("listbox");
118
+ expect(trigger().getAttribute("aria-expanded")).toBe("false");
119
+ if (width === 390) expect(trigger()).toBe(serverTrigger);
120
+ },
121
+ );
122
+
123
+ it("desktop: keyboard opens on the selected option, navigates, selects and closes", async () => {
124
+ await hydrateAt(1200);
125
+ await key(trigger(), "Enter");
126
+ await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
127
+ expect(listOptions().map((node) => node.textContent?.trim())).toEqual(options);
128
+ expect(option("Beta").getAttribute("aria-selected")).toBe("true");
129
+ expect(document.querySelector('[role="listbox"]')).toBeTruthy();
130
+
131
+ await waitFor(() => expect(focused()).toBe(option("Beta")));
132
+ await key(focused(), "ArrowDown");
133
+ await waitFor(() => expect(focused()).toBe(option("Gamma")));
134
+ await key(focused(), "Enter");
135
+ await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("false"));
136
+ expect(onChange).toHaveBeenCalledExactlyOnceWith("Gamma");
137
+ expect(container?.querySelector("output")?.textContent).toBe("Gamma");
138
+ expect(trigger().textContent).toContain("Gamma");
139
+ });
140
+
141
+ it("desktop: Escape closes without changing the value", async () => {
142
+ await hydrateAt(1200);
143
+ await key(trigger(), "ArrowDown");
144
+ await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
145
+ await key(document.activeElement ?? document.body, "Escape");
146
+ await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("false"));
147
+ expect(onChange).not.toHaveBeenCalled();
148
+ expect(trigger().textContent).toContain("Beta");
149
+ });
150
+
151
+ it("desktop: pointer opens, selects, and an outside press dismisses", async () => {
152
+ await hydrateAt(1200);
153
+ await press(trigger());
154
+ await waitFor(() => expect(listOptions().length).toBe(options.length));
155
+ await press(option("Delta"));
156
+ await waitFor(() => expect(container?.querySelector("output")?.textContent).toBe("Delta"));
157
+ expect(onChange).toHaveBeenCalledExactlyOnceWith("Delta");
158
+
159
+ await press(trigger());
160
+ await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
161
+ await press(document.body);
162
+ await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("false"));
163
+ expect(onChange).toHaveBeenCalledTimes(1);
164
+ });
165
+
166
+ it("phone: the sheet opens by pointer and selects, and wires aria-controls while open", async () => {
167
+ await hydrateAt(390);
168
+ await press(trigger());
169
+ await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
170
+ const controls = trigger().getAttribute("aria-controls");
171
+ expect(controls).toBeTruthy();
172
+ expect(document.getElementById(controls!)?.getAttribute("role")).toBe("listbox");
173
+ await press(option("Alpha"));
174
+ await waitFor(() => expect(container?.querySelector("output")?.textContent).toBe("Alpha"));
175
+ expect(onChange).toHaveBeenCalledExactlyOnceWith("Alpha");
176
+ });
177
+ });
@@ -1,117 +1,18 @@
1
1
  /**
2
- * DocType meta shapes as `frappe.desk.form.load.getdoctype` returns them.
3
- *
4
- * These were declared in `@multiplatform.one/backoffice`
5
- * (`shared/useDoctypeMeta.ts`) while the desk form engine lived there. The
6
- * engine now lives here (MPO-15), so the shapes it consumes live here too —
7
- * a lower-tier package must never reach up into backoffice for a type.
8
- * `useDoctypeMeta` (the loader hook, which needs the backoffice app context)
9
- * stays where it is and re-exports these.
10
- *
11
- * Deliberately loose: the server is the source of truth, `0 | 1` flags are
12
- * what the RPC actually sends, and the index signature carries the keys no
13
- * surface has needed yet. `fieldtype` stays `string` here; narrowing it to
14
- * the `FrappeFieldType` union is the remaining half of MPO-15's meta-model
15
- * criterion and is tracked there, not silently done in a move.
2
+ * The desk's names for the one DocType meta model, which
3
+ * `@multiplatform.one/frappe` declares (MPO-15). These are aliases, not
4
+ * shapes: the form engine, the backoffice surfaces and `useDocTypeMeta`
5
+ * all read the same `DocTypeField` / `DocTypeMeta`, so a field key the
6
+ * server sends reaches every one of them or none.
16
7
  */
8
+ import type {
9
+ DocTypeField,
10
+ DocTypeLink,
11
+ DocTypeMeta,
12
+ DocTypePermission,
13
+ } from "@multiplatform.one/frappe";
17
14
 
18
- /** A single DocField row from a DocType meta (loose — server is source of truth). */
19
- export interface DocFieldMeta {
20
- fieldname: string;
21
- label?: string;
22
- fieldtype: string;
23
- options?: string;
24
- reqd?: 0 | 1;
25
- hidden?: 0 | 1;
26
- read_only?: 0 | 1;
27
- bold?: 0 | 1;
28
- in_list_view?: 0 | 1;
29
- in_standard_filter?: 0 | 1;
30
- in_global_search?: 0 | 1;
31
- allow_in_quick_entry?: 0 | 1;
32
- allow_on_submit?: 0 | 1;
33
- no_copy?: 0 | 1;
34
- collapsible?: 0 | 1;
35
- collapsible_depends_on?: string;
36
- depends_on?: string;
37
- mandatory_depends_on?: string;
38
- read_only_depends_on?: string;
39
- fetch_from?: string;
40
- permlevel?: number;
41
- length?: number;
42
- default?: string;
43
- description?: string;
44
- precision?: string | number;
45
- idx?: number;
46
- [key: string]: unknown;
47
- }
48
-
49
- /** DocType Link child row (Connections tab). Probed: `group` arrives null when unset. */
50
- export interface DocLinkMeta {
51
- name?: string;
52
- link_doctype: string;
53
- link_fieldname: string;
54
- group?: string | null;
55
- hidden?: 0 | 1;
56
- is_child_table?: 0 | 1;
57
- parent_doctype?: string;
58
- table_fieldname?: string;
59
- [key: string]: unknown;
60
- }
61
-
62
- /** Role permission row from the DocType meta. */
63
- export interface DocPermissionRule {
64
- role?: string;
65
- permlevel?: number;
66
- read?: 0 | 1;
67
- write?: 0 | 1;
68
- create?: 0 | 1;
69
- delete?: 0 | 1;
70
- submit?: 0 | 1;
71
- cancel?: 0 | 1;
72
- amend?: 0 | 1;
73
- print?: 0 | 1;
74
- email?: 0 | 1;
75
- share?: 0 | 1;
76
- if_owner?: 0 | 1;
77
- [key: string]: unknown;
78
- }
79
-
80
- /** DocType meta as returned by frappe.desk.form.load.getdoctype (loose). */
81
- export interface DoctypeMeta {
82
- doctype?: string;
83
- name: string;
84
- module?: string;
85
- issingle?: 0 | 1;
86
- istable?: 0 | 1;
87
- is_submittable?: 0 | 1;
88
- is_tree?: 0 | 1;
89
- /** Desk treeview_settings asset (`{doctype}_tree.js`) — gates Tree. */
90
- __tree_js?: string;
91
- /** Desk calendar_settings asset (`{doctype}_calendar.js`) — gates Calendar. */
92
- __calendar_js?: string;
93
- is_virtual?: 0 | 1;
94
- editable_grid?: 0 | 1;
95
- track_changes?: 0 | 1;
96
- quick_entry?: 0 | 1;
97
- autoname?: string;
98
- naming_rule?: string;
99
- allow_rename?: 0 | 1;
100
- allow_copy?: 0 | 1;
101
- read_only?: 0 | 1;
102
- in_create?: 0 | 1;
103
- hide_toolbar?: 0 | 1;
104
- max_attachments?: number;
105
- title_field?: string;
106
- image_field?: string;
107
- search_fields?: string;
108
- sort_field?: string;
109
- sort_order?: string;
110
- icon?: string;
111
- document_type?: string;
112
- fields: DocFieldMeta[];
113
- permissions?: DocPermissionRule[];
114
- links?: DocLinkMeta[];
115
- states?: Array<{ title?: string; color?: string } & Record<string, unknown>>;
116
- [key: string]: unknown;
117
- }
15
+ export type DocFieldMeta = DocTypeField;
16
+ export type DocLinkMeta = DocTypeLink;
17
+ export type DocPermissionRule = DocTypePermission;
18
+ export type DoctypeMeta = DocTypeMeta;
@@ -31,3 +31,5 @@ export type { BuildTabsOptions, FormTab } from "./formLayout";
31
31
 
32
32
  export { useFormEngine } from "./useFormEngine";
33
33
  export type { FormEngine } from "./useFormEngine";
34
+
35
+ export { useSessionRoles } from "./useSessionRoles";
@@ -20,9 +20,17 @@
20
20
  * modules directly, so importing the engine never drags SchemaForm,
21
21
  * FrappeTable and the whole table/native stack into a node-ish consumer.
22
22
  */
23
+ import type {
24
+ DocTypeField,
25
+ DocTypeLink,
26
+ DocTypeMeta,
27
+ DocTypePermission,
28
+ FrappeFieldType,
29
+ } from "@multiplatform.one/frappe";
23
30
  import fs from "node:fs";
24
31
  import path from "node:path";
25
- import { describe, expect, it } from "vitest";
32
+ import { describe, expect, expectTypeOf, it } from "vitest";
33
+ import type { DocFieldMeta, DocLinkMeta, DocPermissionRule, DoctypeMeta } from "./doctypeMeta";
26
34
  import * as formEngine from "./index";
27
35
 
28
36
  const engineDir = path.resolve(__dirname);
@@ -129,10 +137,21 @@ describe("form-engine seam (MPO-15)", () => {
129
137
  }
130
138
  });
131
139
 
132
- it("keeps the meta shapes here, where the engine that consumes them lives", () => {
140
+ it("takes the one meta model from @multiplatform.one/frappe and declares none of its own", () => {
141
+ expectTypeOf<DocFieldMeta>().toEqualTypeOf<DocTypeField>();
142
+ expectTypeOf<DoctypeMeta>().toEqualTypeOf<DocTypeMeta>();
143
+ expectTypeOf<DocPermissionRule>().toEqualTypeOf<DocTypePermission>();
144
+ expectTypeOf<DocLinkMeta>().toEqualTypeOf<DocTypeLink>();
145
+ expectTypeOf<DocFieldMeta["fieldtype"]>().toEqualTypeOf<FrappeFieldType>();
146
+
133
147
  const meta = fs.readFileSync(path.resolve(engineDir, "doctypeMeta.ts"), "utf-8");
134
- for (const name of ["DocFieldMeta", "DocLinkMeta", "DocPermissionRule", "DoctypeMeta"]) {
135
- expect(meta, name).toContain(`export interface ${name}`);
148
+ expect(meta).toMatch(/from ["']@multiplatform\.one\/frappe["']/);
149
+ for (const file of sourceFiles(srcDir)) {
150
+ if (file.includes(".spec.") || file.includes(".stories.")) continue;
151
+ const source = fs.readFileSync(file, "utf-8");
152
+ expect(source, file).not.toMatch(
153
+ /export interface (DocFieldMeta|DoctypeMeta|DocTypeField|DocTypeMeta)\b/,
154
+ );
136
155
  }
137
156
  // The shapes reach the engine by relative import, never through the barrel.
138
157
  for (const file of [
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Session user's role list — the client-side half of desk's
3
+ * `frappe.user_roles` (bootinfo). Fetched once per site via the whitelisted
4
+ * `frappe.core.doctype.user.user.get_roles` RPC (probed:
5
+ * `?uid=<user>` → `{"message": ["Role", ...]}`), keyed off
6
+ * `frappe.auth.get_logged_user`.
7
+ *
8
+ * Undefined while loading, on a fixtures-only connection, or when the RPC
9
+ * fails. The permlevel matrix then takes the conservative
10
+ * "permlevel > 0 → read-only" rule (fieldPermissions.ts).
11
+ */
12
+
13
+ import { useEffect, useRef, useState } from "react";
14
+ import { createHttpClient, type FrappeConnection } from "./formActions";
15
+
16
+ const rolesCache = new Map<string, Promise<string[]>>();
17
+
18
+ function fetchSessionRoles(connection: FrappeConnection): Promise<string[]> {
19
+ const http = createHttpClient(connection);
20
+ return http
21
+ .get<{ message?: string }>("/api/method/frappe.auth.get_logged_user")
22
+ .then((res) => {
23
+ const user = res.payload?.message;
24
+ if (!user) throw new Error("No session user");
25
+ return http.get<{ message?: string[] }>(
26
+ "/api/method/frappe.core.doctype.user.user.get_roles",
27
+ { uid: user },
28
+ );
29
+ })
30
+ .then((res) => {
31
+ const list = res.payload?.message;
32
+ if (!Array.isArray(list)) throw new Error("No roles returned");
33
+ return list;
34
+ });
35
+ }
36
+
37
+ /** A connection that may not name a bench yet (a provider's config before it resolves). */
38
+ type SessionConnection = Omit<FrappeConnection, "baseURL"> & { baseURL?: string };
39
+
40
+ export function useSessionRoles(connection: SessionConnection | undefined): string[] | undefined {
41
+ const baseURL = connection?.baseURL;
42
+ const fixtures = connection?.fixtures;
43
+ const [roles, setRoles] = useState<string[] | undefined>(undefined);
44
+ const connectionRef = useRef(connection);
45
+ connectionRef.current = connection;
46
+
47
+ useEffect(() => {
48
+ const current = connectionRef.current;
49
+ if (!current || fixtures || !baseURL) {
50
+ setRoles(undefined);
51
+ return;
52
+ }
53
+ let cancelled = false;
54
+ let promise = rolesCache.get(baseURL);
55
+ if (!promise) {
56
+ promise = fetchSessionRoles({ ...current, baseURL });
57
+ rolesCache.set(baseURL, promise);
58
+ promise.catch(() => rolesCache.delete(baseURL));
59
+ }
60
+ promise
61
+ .then((list) => {
62
+ if (!cancelled) setRoles(list);
63
+ })
64
+ .catch(() => {
65
+ if (!cancelled) setRoles(undefined);
66
+ });
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [baseURL, fixtures]);
71
+
72
+ return roles;
73
+ }