@multiplatform.one/frappe-ui 7.28.1 → 7.29.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 (325) hide show
  1. package/dist/cjs/FrappeCalendar.cjs +3 -2
  2. package/dist/cjs/FrappeCalendar.native.js +3 -2
  3. package/dist/cjs/FrappeCalendar.native.js.map +1 -1
  4. package/dist/cjs/FrappeForm.cjs +148 -464
  5. package/dist/cjs/FrappeForm.native.js +152 -633
  6. package/dist/cjs/FrappeForm.native.js.map +1 -1
  7. package/dist/cjs/FrappeListView.cjs +5 -27
  8. package/dist/cjs/FrappeListView.native.js +11 -32
  9. package/dist/cjs/FrappeListView.native.js.map +1 -1
  10. package/dist/cjs/FrappeReportBuilder.cjs +523 -309
  11. package/dist/cjs/FrappeReportBuilder.native.js +658 -353
  12. package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
  13. package/dist/cjs/FrappeReportView.cjs +35 -234
  14. package/dist/cjs/FrappeReportView.native.js +39 -283
  15. package/dist/cjs/FrappeReportView.native.js.map +1 -1
  16. package/dist/cjs/dependsOn.cjs +2 -2
  17. package/dist/cjs/dependsOn.native.js +1 -2
  18. package/dist/cjs/dependsOn.native.js.map +1 -1
  19. package/dist/cjs/fields/shared/EditorPreviewPanes.cjs +2 -2
  20. package/dist/cjs/fields/shared/EditorPreviewPanes.native.js +2 -2
  21. package/dist/cjs/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  22. package/dist/cjs/formEngine/docPermissions.cjs +20 -0
  23. package/dist/cjs/formEngine/docPermissions.native.js +69 -0
  24. package/dist/cjs/formEngine/docPermissions.native.js.map +1 -1
  25. package/dist/cjs/formEngine/index.cjs +2 -0
  26. package/dist/cjs/formEngine/index.native.js +2 -0
  27. package/dist/cjs/formEngine/index.native.js.map +1 -1
  28. package/dist/cjs/index.cjs +67 -7
  29. package/dist/cjs/index.native.js +67 -7
  30. package/dist/cjs/index.native.js.map +1 -1
  31. package/dist/cjs/liveErrors.cjs +8 -2
  32. package/dist/cjs/liveErrors.native.js +8 -2
  33. package/dist/cjs/liveErrors.native.js.map +1 -1
  34. package/dist/cjs/measures.cjs +31 -0
  35. package/dist/cjs/measures.native.js +34 -0
  36. package/dist/cjs/measures.native.js.map +1 -0
  37. package/dist/cjs/recordShape.cjs +103 -0
  38. package/dist/cjs/recordShape.native.js +115 -0
  39. package/dist/cjs/recordShape.native.js.map +1 -0
  40. package/dist/cjs/report/ReportGrid.cjs +453 -0
  41. package/dist/cjs/report/ReportGrid.native.js +510 -0
  42. package/dist/cjs/report/ReportGrid.native.js.map +1 -0
  43. package/dist/cjs/report/ReportViewToolbar.cjs +253 -0
  44. package/dist/cjs/report/ReportViewToolbar.native.js +303 -0
  45. package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -0
  46. package/dist/cjs/report/queryReport.cjs +444 -0
  47. package/dist/cjs/report/queryReport.native.js +588 -0
  48. package/dist/cjs/report/queryReport.native.js.map +1 -0
  49. package/dist/cjs/report/queryReportColumns.cjs +131 -0
  50. package/dist/cjs/report/queryReportColumns.native.js +159 -0
  51. package/dist/cjs/report/queryReportColumns.native.js.map +1 -0
  52. package/dist/cjs/report/reportViewModel.cjs +386 -0
  53. package/dist/cjs/report/reportViewModel.native.js +657 -0
  54. package/dist/cjs/report/reportViewModel.native.js.map +1 -0
  55. package/dist/cjs/report/savedReports.cjs +137 -0
  56. package/dist/cjs/report/savedReports.native.js +169 -0
  57. package/dist/cjs/report/savedReports.native.js.map +1 -0
  58. package/dist/cjs/report/useFrappeReport.cjs +184 -0
  59. package/dist/cjs/report/useFrappeReport.native.js +214 -0
  60. package/dist/cjs/report/useFrappeReport.native.js.map +1 -0
  61. package/dist/cjs/report/useQueryReport.cjs +201 -0
  62. package/dist/cjs/report/useQueryReport.native.js +233 -0
  63. package/dist/cjs/report/useQueryReport.native.js.map +1 -0
  64. package/dist/cjs/useFrappeForm.cjs +558 -0
  65. package/dist/cjs/useFrappeForm.native.js +714 -0
  66. package/dist/cjs/useFrappeForm.native.js.map +1 -0
  67. package/dist/esm/FrappeCalendar.mjs +4 -3
  68. package/dist/esm/FrappeCalendar.mjs.map +1 -1
  69. package/dist/esm/FrappeCalendar.native.js +4 -3
  70. package/dist/esm/FrappeCalendar.native.js.map +1 -1
  71. package/dist/esm/FrappeForm.mjs +151 -467
  72. package/dist/esm/FrappeForm.mjs.map +1 -1
  73. package/dist/esm/FrappeForm.native.js +155 -636
  74. package/dist/esm/FrappeForm.native.js.map +1 -1
  75. package/dist/esm/FrappeListView.mjs +6 -28
  76. package/dist/esm/FrappeListView.mjs.map +1 -1
  77. package/dist/esm/FrappeListView.native.js +12 -33
  78. package/dist/esm/FrappeListView.native.js.map +1 -1
  79. package/dist/esm/FrappeReportBuilder.mjs +526 -312
  80. package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
  81. package/dist/esm/FrappeReportBuilder.native.js +661 -356
  82. package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
  83. package/dist/esm/FrappeReportView.mjs +30 -229
  84. package/dist/esm/FrappeReportView.mjs.map +1 -1
  85. package/dist/esm/FrappeReportView.native.js +34 -278
  86. package/dist/esm/FrappeReportView.native.js.map +1 -1
  87. package/dist/esm/dependsOn.mjs +2 -2
  88. package/dist/esm/dependsOn.mjs.map +1 -1
  89. package/dist/esm/dependsOn.native.js +1 -2
  90. package/dist/esm/dependsOn.native.js.map +1 -1
  91. package/dist/esm/fields/shared/EditorPreviewPanes.mjs +2 -2
  92. package/dist/esm/fields/shared/EditorPreviewPanes.mjs.map +1 -1
  93. package/dist/esm/fields/shared/EditorPreviewPanes.native.js +2 -2
  94. package/dist/esm/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  95. package/dist/esm/formEngine/docPermissions.mjs +19 -1
  96. package/dist/esm/formEngine/docPermissions.mjs.map +1 -1
  97. package/dist/esm/formEngine/docPermissions.native.js +68 -1
  98. package/dist/esm/formEngine/docPermissions.native.js.map +1 -1
  99. package/dist/esm/formEngine/index.mjs +2 -2
  100. package/dist/esm/formEngine/index.mjs.map +1 -1
  101. package/dist/esm/formEngine/index.native.js +2 -2
  102. package/dist/esm/formEngine/index.native.js.map +1 -1
  103. package/dist/esm/index.mjs +16 -4
  104. package/dist/esm/index.mjs.map +1 -1
  105. package/dist/esm/index.native.js +16 -4
  106. package/dist/esm/index.native.js.map +1 -1
  107. package/dist/esm/liveErrors.mjs +8 -3
  108. package/dist/esm/liveErrors.mjs.map +1 -1
  109. package/dist/esm/liveErrors.native.js +8 -3
  110. package/dist/esm/liveErrors.native.js.map +1 -1
  111. package/dist/esm/measures.mjs +5 -0
  112. package/dist/esm/measures.mjs.map +1 -0
  113. package/dist/esm/measures.native.js +5 -0
  114. package/dist/esm/measures.native.js.map +1 -0
  115. package/dist/esm/recordShape.mjs +74 -0
  116. package/dist/esm/recordShape.mjs.map +1 -0
  117. package/dist/esm/recordShape.native.js +83 -0
  118. package/dist/esm/recordShape.native.js.map +1 -0
  119. package/dist/esm/report/ReportGrid.mjs +428 -0
  120. package/dist/esm/report/ReportGrid.mjs.map +1 -0
  121. package/dist/esm/report/ReportGrid.native.js +471 -0
  122. package/dist/esm/report/ReportGrid.native.js.map +1 -0
  123. package/dist/esm/report/ReportViewToolbar.mjs +228 -0
  124. package/dist/esm/report/ReportViewToolbar.mjs.map +1 -0
  125. package/dist/esm/report/ReportViewToolbar.native.js +264 -0
  126. package/dist/esm/report/ReportViewToolbar.native.js.map +1 -0
  127. package/dist/esm/report/queryReport.mjs +404 -0
  128. package/dist/esm/report/queryReport.mjs.map +1 -0
  129. package/dist/esm/report/queryReport.native.js +545 -0
  130. package/dist/esm/report/queryReport.native.js.map +1 -0
  131. package/dist/esm/report/queryReportColumns.mjs +105 -0
  132. package/dist/esm/report/queryReportColumns.mjs.map +1 -0
  133. package/dist/esm/report/queryReportColumns.native.js +119 -0
  134. package/dist/esm/report/queryReportColumns.native.js.map +1 -0
  135. package/dist/esm/report/reportViewModel.mjs +343 -0
  136. package/dist/esm/report/reportViewModel.mjs.map +1 -0
  137. package/dist/esm/report/reportViewModel.native.js +611 -0
  138. package/dist/esm/report/reportViewModel.native.js.map +1 -0
  139. package/dist/esm/report/savedReports.mjs +112 -0
  140. package/dist/esm/report/savedReports.mjs.map +1 -0
  141. package/dist/esm/report/savedReports.native.js +141 -0
  142. package/dist/esm/report/savedReports.native.js.map +1 -0
  143. package/dist/esm/report/useFrappeReport.mjs +158 -0
  144. package/dist/esm/report/useFrappeReport.mjs.map +1 -0
  145. package/dist/esm/report/useFrappeReport.native.js +185 -0
  146. package/dist/esm/report/useFrappeReport.native.js.map +1 -0
  147. package/dist/esm/report/useQueryReport.mjs +176 -0
  148. package/dist/esm/report/useQueryReport.mjs.map +1 -0
  149. package/dist/esm/report/useQueryReport.native.js +205 -0
  150. package/dist/esm/report/useQueryReport.native.js.map +1 -0
  151. package/dist/esm/useFrappeForm.mjs +533 -0
  152. package/dist/esm/useFrappeForm.mjs.map +1 -0
  153. package/dist/esm/useFrappeForm.native.js +686 -0
  154. package/dist/esm/useFrappeForm.native.js.map +1 -0
  155. package/dist/jsx/FrappeCalendar.mjs +4 -3
  156. package/dist/jsx/FrappeCalendar.mjs.map +1 -1
  157. package/dist/jsx/FrappeCalendar.native.js +3 -2
  158. package/dist/jsx/FrappeCalendar.native.js.map +1 -1
  159. package/dist/jsx/FrappeForm.mjs +151 -467
  160. package/dist/jsx/FrappeForm.mjs.map +1 -1
  161. package/dist/jsx/FrappeForm.native.js +152 -633
  162. package/dist/jsx/FrappeForm.native.js.map +1 -1
  163. package/dist/jsx/FrappeListView.mjs +6 -28
  164. package/dist/jsx/FrappeListView.mjs.map +1 -1
  165. package/dist/jsx/FrappeListView.native.js +11 -32
  166. package/dist/jsx/FrappeListView.native.js.map +1 -1
  167. package/dist/jsx/FrappeReportBuilder.mjs +526 -312
  168. package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
  169. package/dist/jsx/FrappeReportBuilder.native.js +658 -353
  170. package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
  171. package/dist/jsx/FrappeReportView.mjs +30 -229
  172. package/dist/jsx/FrappeReportView.mjs.map +1 -1
  173. package/dist/jsx/FrappeReportView.native.js +39 -283
  174. package/dist/jsx/FrappeReportView.native.js.map +1 -1
  175. package/dist/jsx/dependsOn.mjs +2 -2
  176. package/dist/jsx/dependsOn.mjs.map +1 -1
  177. package/dist/jsx/dependsOn.native.js +1 -2
  178. package/dist/jsx/dependsOn.native.js.map +1 -1
  179. package/dist/jsx/fields/shared/EditorPreviewPanes.mjs +2 -2
  180. package/dist/jsx/fields/shared/EditorPreviewPanes.mjs.map +1 -1
  181. package/dist/jsx/fields/shared/EditorPreviewPanes.native.js +2 -2
  182. package/dist/jsx/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  183. package/dist/jsx/formEngine/docPermissions.mjs +19 -1
  184. package/dist/jsx/formEngine/docPermissions.mjs.map +1 -1
  185. package/dist/jsx/formEngine/docPermissions.native.js +69 -0
  186. package/dist/jsx/formEngine/docPermissions.native.js.map +1 -1
  187. package/dist/jsx/formEngine/index.mjs +2 -2
  188. package/dist/jsx/formEngine/index.mjs.map +1 -1
  189. package/dist/jsx/formEngine/index.native.js +2 -0
  190. package/dist/jsx/formEngine/index.native.js.map +1 -1
  191. package/dist/jsx/index.js +16 -4
  192. package/dist/jsx/index.js.map +1 -1
  193. package/dist/jsx/index.mjs +16 -4
  194. package/dist/jsx/index.mjs.map +1 -1
  195. package/dist/jsx/index.native.js +67 -7
  196. package/dist/jsx/index.native.js.map +1 -1
  197. package/dist/jsx/liveErrors.mjs +8 -3
  198. package/dist/jsx/liveErrors.mjs.map +1 -1
  199. package/dist/jsx/liveErrors.native.js +8 -2
  200. package/dist/jsx/liveErrors.native.js.map +1 -1
  201. package/dist/jsx/measures.mjs +5 -0
  202. package/dist/jsx/measures.mjs.map +1 -0
  203. package/dist/jsx/measures.native.js +34 -0
  204. package/dist/jsx/measures.native.js.map +1 -0
  205. package/dist/jsx/recordShape.mjs +74 -0
  206. package/dist/jsx/recordShape.mjs.map +1 -0
  207. package/dist/jsx/recordShape.native.js +115 -0
  208. package/dist/jsx/recordShape.native.js.map +1 -0
  209. package/dist/jsx/report/ReportGrid.mjs +428 -0
  210. package/dist/jsx/report/ReportGrid.mjs.map +1 -0
  211. package/dist/jsx/report/ReportGrid.native.js +510 -0
  212. package/dist/jsx/report/ReportGrid.native.js.map +1 -0
  213. package/dist/jsx/report/ReportViewToolbar.mjs +228 -0
  214. package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -0
  215. package/dist/jsx/report/ReportViewToolbar.native.js +303 -0
  216. package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -0
  217. package/dist/jsx/report/queryReport.mjs +404 -0
  218. package/dist/jsx/report/queryReport.mjs.map +1 -0
  219. package/dist/jsx/report/queryReport.native.js +588 -0
  220. package/dist/jsx/report/queryReport.native.js.map +1 -0
  221. package/dist/jsx/report/queryReportColumns.mjs +105 -0
  222. package/dist/jsx/report/queryReportColumns.mjs.map +1 -0
  223. package/dist/jsx/report/queryReportColumns.native.js +159 -0
  224. package/dist/jsx/report/queryReportColumns.native.js.map +1 -0
  225. package/dist/jsx/report/reportViewModel.mjs +343 -0
  226. package/dist/jsx/report/reportViewModel.mjs.map +1 -0
  227. package/dist/jsx/report/reportViewModel.native.js +657 -0
  228. package/dist/jsx/report/reportViewModel.native.js.map +1 -0
  229. package/dist/jsx/report/savedReports.mjs +112 -0
  230. package/dist/jsx/report/savedReports.mjs.map +1 -0
  231. package/dist/jsx/report/savedReports.native.js +169 -0
  232. package/dist/jsx/report/savedReports.native.js.map +1 -0
  233. package/dist/jsx/report/useFrappeReport.mjs +158 -0
  234. package/dist/jsx/report/useFrappeReport.mjs.map +1 -0
  235. package/dist/jsx/report/useFrappeReport.native.js +214 -0
  236. package/dist/jsx/report/useFrappeReport.native.js.map +1 -0
  237. package/dist/jsx/report/useQueryReport.mjs +176 -0
  238. package/dist/jsx/report/useQueryReport.mjs.map +1 -0
  239. package/dist/jsx/report/useQueryReport.native.js +233 -0
  240. package/dist/jsx/report/useQueryReport.native.js.map +1 -0
  241. package/dist/jsx/useFrappeForm.mjs +533 -0
  242. package/dist/jsx/useFrappeForm.mjs.map +1 -0
  243. package/dist/jsx/useFrappeForm.native.js +714 -0
  244. package/dist/jsx/useFrappeForm.native.js.map +1 -0
  245. package/package.json +16 -16
  246. package/src/FrappeCalendar.tsx +8 -2
  247. package/src/FrappeForm.stories.tsx +52 -0
  248. package/src/FrappeForm.tsx +176 -565
  249. package/src/FrappeListView.cards.stories.tsx +3 -6
  250. package/src/FrappeListView.tsx +15 -55
  251. package/src/FrappeReportBuilder.stories.tsx +34 -7
  252. package/src/FrappeReportBuilder.tsx +790 -380
  253. package/src/FrappeReportView.spec.tsx +10 -6
  254. package/src/FrappeReportView.stories.tsx +20 -1
  255. package/src/FrappeReportView.tsx +89 -415
  256. package/src/dependsOn.ts +4 -1
  257. package/src/fields/shared/EditorPreviewPanes.tsx +2 -2
  258. package/src/formEngine/docPermissions.spec.ts +36 -1
  259. package/src/formEngine/docPermissions.ts +34 -0
  260. package/src/formEngine/index.ts +8 -1
  261. package/src/frappeRemainder.designLaw.spec.ts +9 -1
  262. package/src/frappeRoutes.spec.tsx +9 -0
  263. package/src/frappeViews.designLaw.spec.ts +15 -2
  264. package/src/guestFallback.spec.tsx +236 -0
  265. package/src/index.ts +117 -16
  266. package/src/liveErrors.spec.ts +10 -0
  267. package/src/liveErrors.ts +16 -3
  268. package/src/measures.ts +11 -0
  269. package/src/recordShape.spec.ts +100 -0
  270. package/src/recordShape.ts +133 -0
  271. package/src/report/ReportGrid.spec.tsx +175 -0
  272. package/src/report/ReportGrid.tsx +530 -0
  273. package/src/report/ReportViewToolbar.spec.tsx +90 -0
  274. package/src/report/ReportViewToolbar.tsx +314 -0
  275. package/src/report/queryReport.spec.ts +483 -0
  276. package/src/report/queryReport.ts +722 -0
  277. package/src/report/queryReportColumns.tsx +123 -0
  278. package/src/report/reportViewModel.spec.ts +393 -0
  279. package/src/report/reportViewModel.ts +617 -0
  280. package/src/report/savedReports.ts +172 -0
  281. package/src/report/seam.spec.ts +216 -0
  282. package/src/report/useFrappeReport.ts +241 -0
  283. package/src/report/useQueryReport.spec.tsx +131 -0
  284. package/src/report/useQueryReport.ts +309 -0
  285. package/src/useFrappeForm.spec.tsx +209 -0
  286. package/src/useFrappeForm.ts +778 -0
  287. package/types/FrappeCalendar.d.ts.map +1 -1
  288. package/types/FrappeForm.d.ts +21 -1
  289. package/types/FrappeForm.d.ts.map +1 -1
  290. package/types/FrappeListView.d.ts.map +1 -1
  291. package/types/FrappeReportBuilder.d.ts +66 -16
  292. package/types/FrappeReportBuilder.d.ts.map +1 -1
  293. package/types/FrappeReportView.d.ts +9 -127
  294. package/types/FrappeReportView.d.ts.map +1 -1
  295. package/types/dependsOn.d.ts.map +1 -1
  296. package/types/formEngine/docPermissions.d.ts +14 -0
  297. package/types/formEngine/docPermissions.d.ts.map +1 -1
  298. package/types/formEngine/index.d.ts +1 -1
  299. package/types/formEngine/index.d.ts.map +1 -1
  300. package/types/index.d.ts +15 -3
  301. package/types/index.d.ts.map +1 -1
  302. package/types/liveErrors.d.ts +9 -1
  303. package/types/liveErrors.d.ts.map +1 -1
  304. package/types/measures.d.ts +3 -0
  305. package/types/measures.d.ts.map +1 -0
  306. package/types/recordShape.d.ts +45 -0
  307. package/types/recordShape.d.ts.map +1 -0
  308. package/types/report/ReportGrid.d.ts +20 -0
  309. package/types/report/ReportGrid.d.ts.map +1 -0
  310. package/types/report/ReportViewToolbar.d.ts +31 -0
  311. package/types/report/ReportViewToolbar.d.ts.map +1 -0
  312. package/types/report/queryReport.d.ts +181 -0
  313. package/types/report/queryReport.d.ts.map +1 -0
  314. package/types/report/queryReportColumns.d.ts +12 -0
  315. package/types/report/queryReportColumns.d.ts.map +1 -0
  316. package/types/report/reportViewModel.d.ts +146 -0
  317. package/types/report/reportViewModel.d.ts.map +1 -0
  318. package/types/report/savedReports.d.ts +27 -0
  319. package/types/report/savedReports.d.ts.map +1 -0
  320. package/types/report/useFrappeReport.d.ts +60 -0
  321. package/types/report/useFrappeReport.d.ts.map +1 -0
  322. package/types/report/useQueryReport.d.ts +37 -0
  323. package/types/report/useQueryReport.d.ts.map +1 -0
  324. package/types/useFrappeForm.d.ts +142 -0
  325. package/types/useFrappeForm.d.ts.map +1 -0
@@ -0,0 +1,100 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { fieldsFromRecord, recordFieldKeys, recordKeyLabel, useRecordShape } from "./recordShape";
4
+
5
+ const record = {
6
+ name: "a1b2c3",
7
+ doctype: "Poke Move",
8
+ owner: "Administrator",
9
+ creation: "2026-09-29 09:00:00",
10
+ modified: "2026-09-29 10:00:00",
11
+ modified_by: "Administrator",
12
+ docstatus: 0,
13
+ idx: 0,
14
+ _user_tags: null,
15
+ _comments: null,
16
+ _assign: null,
17
+ _liked_by: null,
18
+ _seen: null,
19
+ $origin: "remote",
20
+ $key: "a1b2c3",
21
+ move_name: "tackle",
22
+ power: 40,
23
+ accuracy: 0.95,
24
+ damage_class: null,
25
+ effects: [{ name: "row-1" }],
26
+ };
27
+
28
+ describe("recordShape", () => {
29
+ it("keeps a record's own scalar keys in server order", () => {
30
+ expect(recordFieldKeys(record)).toEqual(["move_name", "power", "accuracy", "damage_class"]);
31
+ });
32
+
33
+ it("labels a key the way the desk labels a field", () => {
34
+ expect(recordKeyLabel("base_experience")).toBe("Base Experience");
35
+ });
36
+
37
+ it("types each field from its value and marks it read-only", () => {
38
+ expect(fieldsFromRecord(record)).toEqual([
39
+ { fieldname: "move_name", label: "Move Name", fieldtype: "Data", read_only: 1 },
40
+ { fieldname: "power", label: "Power", fieldtype: "Int", read_only: 1 },
41
+ { fieldname: "accuracy", label: "Accuracy", fieldtype: "Float", read_only: 1 },
42
+ { fieldname: "damage_class", label: "Damage Class", fieldtype: "Data", read_only: 1 },
43
+ ]);
44
+ });
45
+ });
46
+
47
+ describe("useRecordShape", () => {
48
+ afterEach(() => vi.unstubAllGlobals());
49
+
50
+ function stub(answer: () => Response) {
51
+ const reads: string[] = [];
52
+ vi.stubGlobal("fetch", async (input: string | URL | Request) => {
53
+ reads.push(String(input instanceof Request ? input.url : input));
54
+ return answer();
55
+ });
56
+ return reads;
57
+ }
58
+
59
+ it("reads nothing until asked", () => {
60
+ const reads = stub(() => new Response("{}"));
61
+ const { result } = renderHook(() =>
62
+ useRecordShape({ baseURL: "http://shape.test" }, "Poke Move", false),
63
+ );
64
+ expect(result.current).toEqual({ fields: undefined, status: "idle" });
65
+ expect(reads).toEqual([]);
66
+ });
67
+
68
+ it("samples one record with every field", async () => {
69
+ const reads = stub(() => Response.json({ data: [record] }));
70
+ const { result } = renderHook(() =>
71
+ useRecordShape({ baseURL: "http://shape.test" }, "Poke Move", true),
72
+ );
73
+ expect(result.current.status).toBe("pending");
74
+ await waitFor(() => expect(result.current.status).toBe("ready"));
75
+ expect(result.current.fields?.map((field) => field.fieldname)).toEqual([
76
+ "move_name",
77
+ "power",
78
+ "accuracy",
79
+ "damage_class",
80
+ ]);
81
+ const url = new URL(reads[0] ?? "");
82
+ expect(url.pathname).toBe("/api/resource/Poke%20Move");
83
+ expect(url.searchParams.get("fields")).toBe('["*"]');
84
+ expect(url.searchParams.get("limit_page_length")).toBe("1");
85
+ });
86
+
87
+ it("calls a refused or empty doctype unreadable", async () => {
88
+ stub(() => Response.json({ exc_type: "PermissionError" }, { status: 403 }));
89
+ const refused = renderHook(() =>
90
+ useRecordShape({ baseURL: "http://refused.test" }, "Poke Move", true),
91
+ );
92
+ await waitFor(() => expect(refused.result.current.status).toBe("unreadable"));
93
+
94
+ stub(() => Response.json({ data: [] }));
95
+ const empty = renderHook(() =>
96
+ useRecordShape({ baseURL: "http://empty.test" }, "Poke Move", true),
97
+ );
98
+ await waitFor(() => expect(empty.result.current.status).toBe("unreadable"));
99
+ });
100
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * A doctype as far as one of its records shows it.
3
+ *
4
+ * DocType meta is permission-gated (Guest gets 403 on getdoctype) while the
5
+ * records themselves are often readable. A data-aware view that cannot read
6
+ * meta still owes the reader the weakest honest rendering of what they may
7
+ * read (LC-96): the record's own fields, labelled from their keys, typed from
8
+ * their values and read-only. These helpers are that shape, shared by every
9
+ * view that falls back to it.
10
+ */
11
+ import { HttpClient, type AuthConfig, type DocTypeField } from "@multiplatform.one/frappe";
12
+ import { useEffect, useMemo, useState } from "react";
13
+
14
+ /** Keys every Frappe record carries that are bookkeeping, not fields of its doctype. */
15
+ export const recordBookkeepingKeys: ReadonlySet<string> = new Set([
16
+ "name",
17
+ "owner",
18
+ "creation",
19
+ "modified",
20
+ "modified_by",
21
+ "docstatus",
22
+ "idx",
23
+ "doctype",
24
+ "_user_tags",
25
+ "_comments",
26
+ "_assign",
27
+ "_liked_by",
28
+ "_seen",
29
+ ]);
30
+
31
+ function isFieldValue(value: unknown): value is string | number | null {
32
+ return value === null || typeof value === "string" || typeof value === "number";
33
+ }
34
+
35
+ /**
36
+ * A record's own field keys in the order the server sent them. Bookkeeping
37
+ * keys, the live collection's `$` props ($origin, $key, $collectionId) and
38
+ * non-scalar values (child rows, objects) are dropped.
39
+ */
40
+ export function recordFieldKeys(record: Record<string, unknown>): string[] {
41
+ return Object.entries(record)
42
+ .filter(
43
+ ([key, value]) =>
44
+ !key.startsWith("$") && !recordBookkeepingKeys.has(key) && isFieldValue(value),
45
+ )
46
+ .map(([key]) => key);
47
+ }
48
+
49
+ /** "base_experience" → "Base Experience", for a key no meta names. */
50
+ export function recordKeyLabel(key: string): string {
51
+ return key.replace(/_/g, " ").replace(/(^|\s)\S/g, (c) => c.toUpperCase());
52
+ }
53
+
54
+ /**
55
+ * The record's own fields as read-only doctype fields: Int or Float for a
56
+ * number, Data for anything else.
57
+ */
58
+ export function fieldsFromRecord(record: Record<string, unknown>): DocTypeField[] {
59
+ return recordFieldKeys(record).map((key) => {
60
+ const value = record[key];
61
+ return {
62
+ fieldname: key,
63
+ label: recordKeyLabel(key),
64
+ fieldtype: typeof value === "number" ? (Number.isInteger(value) ? "Int" : "Float") : "Data",
65
+ read_only: 1,
66
+ };
67
+ });
68
+ }
69
+
70
+ export interface RecordShapeConnection {
71
+ baseURL?: string;
72
+ auth?: unknown;
73
+ }
74
+
75
+ export interface RecordShape {
76
+ /** The sampled record's fields, once it has been read. */
77
+ fields: DocTypeField[] | undefined;
78
+ /**
79
+ * `idle` when not asked, `pending` while the record is read, `ready` with
80
+ * fields, `unreadable` when the doctype has no record the reader may see.
81
+ */
82
+ status: "idle" | "pending" | "ready" | "unreadable";
83
+ }
84
+
85
+ /**
86
+ * Reads one record of `doctype` (`fields: ["*"]`, one row) and returns its
87
+ * fields. Only reads while `enabled`; a view turns it on when meta is refused.
88
+ */
89
+ export function useRecordShape(
90
+ connection: RecordShapeConnection | undefined,
91
+ doctype: string,
92
+ enabled: boolean,
93
+ ): RecordShape {
94
+ const baseURL = connection?.baseURL;
95
+ const auth = connection?.auth;
96
+ const [sampled, setSampled] = useState<{
97
+ key: string;
98
+ fields: DocTypeField[] | undefined;
99
+ }>();
100
+ const key = `${baseURL ?? ""}\u0000${doctype}`;
101
+ const reads = enabled && Boolean(baseURL);
102
+
103
+ useEffect(() => {
104
+ if (!reads || !baseURL) return;
105
+ let cancelled = false;
106
+ const http = new HttpClient(baseURL, auth as AuthConfig | undefined);
107
+ void (async () => {
108
+ let fields: DocTypeField[] | undefined;
109
+ try {
110
+ const response = await http.get<{ data?: Record<string, unknown>[] }>(
111
+ `/api/resource/${encodeURIComponent(doctype)}`,
112
+ { fields: ["*"], limit_page_length: 1 },
113
+ );
114
+ const sample = response.payload?.data?.[0];
115
+ fields = sample ? fieldsFromRecord(sample) : undefined;
116
+ } catch {
117
+ fields = undefined;
118
+ }
119
+ if (!cancelled) setSampled({ key, fields });
120
+ })();
121
+ return () => {
122
+ cancelled = true;
123
+ };
124
+ }, [reads, baseURL, auth, doctype, key]);
125
+
126
+ return useMemo((): RecordShape => {
127
+ if (!reads) return { fields: undefined, status: "idle" };
128
+ if (sampled?.key !== key) return { fields: undefined, status: "pending" };
129
+ return sampled.fields && sampled.fields.length > 0
130
+ ? { fields: sampled.fields, status: "ready" }
131
+ : { fields: undefined, status: "unreadable" };
132
+ }, [reads, sampled, key]);
133
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * ReportGrid row keyboard contract (LC-49 ONE-TAB-STOP, LC-50
3
+ * KEY-ACTIVATION, DG-A11Y-06 names).
4
+ *
5
+ * The data row is ONE pressable unit: click anywhere activates, Enter AND
6
+ * Space activate from the keyboard, the data-row set exposes exactly one
7
+ * tab stop (roving tabindex, arrows/Home/End move it), rows are named by
8
+ * doc identity, presses on inner interactive content (Link cells) never
9
+ * reach the row handler, and there is no per-cell pressable chrome inside
10
+ * body cells. Group header rows are not pressable and hold no tab stop.
11
+ */
12
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
13
+ import { fireEvent, screen, waitFor } from "@testing-library/react";
14
+ import React from "react";
15
+ import { describe, expect, it, vi } from "vitest";
16
+ import { ReportGrid, type ReportGridProps } from "./ReportGrid";
17
+ import { buildGridRows, type ReportColumnDef, type ReportRow } from "./reportViewModel";
18
+
19
+ const columns: ReportColumnDef[] = [
20
+ { fieldname: "name", label: "ID", fieldtype: "Data", kind: "text", width: 160, align: "left" },
21
+ {
22
+ fieldname: "status",
23
+ label: "Status",
24
+ fieldtype: "Select",
25
+ kind: "select",
26
+ options: "Open\nClosed",
27
+ width: 120,
28
+ align: "left",
29
+ },
30
+ {
31
+ fieldname: "assigned_by",
32
+ label: "Assigned By",
33
+ fieldtype: "Link",
34
+ kind: "link",
35
+ options: "User",
36
+ width: 160,
37
+ align: "left",
38
+ },
39
+ ];
40
+
41
+ const dataRows: ReportRow[] = [
42
+ { name: "TD-0001", status: "Open", assigned_by: "alice@example.com" },
43
+ { name: "TD-0002", status: "Closed", assigned_by: "bob@example.com" },
44
+ { name: "TD-0003", status: "Open", assigned_by: "carol@example.com" },
45
+ ];
46
+
47
+ function pressRows(container: HTMLElement): HTMLElement[] {
48
+ return Array.from(container.querySelectorAll('[data-report-row="true"]')) as HTMLElement[];
49
+ }
50
+
51
+ function renderGrid(overrides: Partial<ReportGridProps> = {}) {
52
+ const onOpenRow = vi.fn();
53
+ const onOpenLink = vi.fn();
54
+ const result = renderWithProviders(
55
+ <ReportGrid
56
+ columns={columns}
57
+ rows={buildGridRows(dataRows, 0, 20)}
58
+ sort={{ field: "modified", order: "desc" }}
59
+ onSortChange={vi.fn()}
60
+ columnFilters={{}}
61
+ onColumnFilterChange={vi.fn()}
62
+ onOpenRow={onOpenRow}
63
+ onOpenLink={onOpenLink}
64
+ totals={[]}
65
+ {...overrides}
66
+ />,
67
+ );
68
+ return { ...result, onOpenRow, onOpenLink };
69
+ }
70
+
71
+ describe("ReportGrid row activation (LC-50)", () => {
72
+ it("activates on a click anywhere in the row with the row's name", () => {
73
+ const { onOpenRow } = renderGrid();
74
+ fireEvent.click(screen.getByText("Closed"));
75
+ expect(onOpenRow).toHaveBeenCalledTimes(1);
76
+ expect(onOpenRow).toHaveBeenCalledWith("TD-0002");
77
+ });
78
+
79
+ it("activates on Enter AND Space, once per physical key press", () => {
80
+ const { container, onOpenRow } = renderGrid();
81
+ const rows = pressRows(container);
82
+ rows[0].focus();
83
+
84
+ fireEvent.keyDown(rows[0], { key: "Enter" });
85
+ expect(onOpenRow).toHaveBeenCalledTimes(1);
86
+ expect(onOpenRow).toHaveBeenLastCalledWith("TD-0001");
87
+
88
+ fireEvent.keyDown(rows[0], { key: " " });
89
+ expect(onOpenRow).toHaveBeenCalledTimes(2);
90
+
91
+ // Key auto-repeat never machine-guns activations.
92
+ fireEvent.keyDown(rows[0], { key: "Enter", repeat: true });
93
+ expect(onOpenRow).toHaveBeenCalledTimes(2);
94
+ });
95
+
96
+ it("names each pressable row by doc identity (DG-A11Y-06)", () => {
97
+ const { container } = renderGrid();
98
+ expect(pressRows(container).map((row) => row.getAttribute("aria-label"))).toEqual([
99
+ "TD-0001",
100
+ "TD-0002",
101
+ "TD-0003",
102
+ ]);
103
+ });
104
+ });
105
+
106
+ describe("ReportGrid roving tab stop (LC-49)", () => {
107
+ it("exposes exactly one tab stop; arrows and Home/End rove it", async () => {
108
+ const { container } = renderGrid();
109
+ const rows = pressRows(container);
110
+ expect(rows).toHaveLength(3);
111
+ expect(rows.map((row) => row.getAttribute("tabindex"))).toEqual(["0", "-1", "-1"]);
112
+
113
+ rows[0].focus();
114
+ expect(document.activeElement).toBe(rows[0]);
115
+
116
+ fireEvent.keyDown(rows[0], { key: "ArrowDown" });
117
+ expect(document.activeElement).toBe(rows[1]);
118
+ // The anchor follows focus: still exactly one tabindex=0.
119
+ await waitFor(() => {
120
+ const stops = pressRows(container).map((row) => row.getAttribute("tabindex"));
121
+ expect(stops).toEqual(["-1", "0", "-1"]);
122
+ });
123
+
124
+ fireEvent.keyDown(rows[1], { key: "End" });
125
+ expect(document.activeElement).toBe(rows[2]);
126
+ fireEvent.keyDown(rows[2], { key: "Home" });
127
+ expect(document.activeElement).toBe(rows[0]);
128
+ fireEvent.keyDown(rows[0], { key: "ArrowUp" });
129
+ // Clamped at the first row — no wrap.
130
+ expect(document.activeElement).toBe(rows[0]);
131
+ });
132
+
133
+ it("group header rows are not pressable and hold no tab stop", () => {
134
+ const { container } = renderGrid({
135
+ rows: buildGridRows(dataRows, 0, 20, "status"),
136
+ groupByColumn: columns[1],
137
+ });
138
+ // Three data rows stay pressable; the injected group headers are not.
139
+ expect(pressRows(container)).toHaveLength(3);
140
+ const groupHeaderCells = Array.from(container.querySelectorAll('[role="rowheader"]'));
141
+ expect(groupHeaderCells.length).toBeGreaterThan(0);
142
+ for (const cell of groupHeaderCells) {
143
+ const groupRow = cell.closest('[role="row"]') as HTMLElement;
144
+ expect(groupRow.getAttribute("data-report-row")).toBeNull();
145
+ expect(groupRow.getAttribute("tabindex")).toBeNull();
146
+ }
147
+ });
148
+ });
149
+
150
+ describe("ReportGrid inner interactive content wins", () => {
151
+ it("a Link cell press opens the linked doc, never the row", () => {
152
+ const { onOpenRow, onOpenLink } = renderGrid();
153
+ fireEvent.click(screen.getByText("alice@example.com"));
154
+ expect(onOpenLink).toHaveBeenCalledTimes(1);
155
+ expect(onOpenLink).toHaveBeenCalledWith("User", "alice@example.com");
156
+ expect(onOpenRow).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it("keys on focused inner content belong to that content, not the row", () => {
160
+ const { onOpenRow } = renderGrid();
161
+ // Keydown dispatched from the link bubbles to the row, where the
162
+ // target !== currentTarget guard hands it back to the inner control.
163
+ fireEvent.keyDown(screen.getByText("alice@example.com"), { key: "Enter" });
164
+ expect(onOpenRow).not.toHaveBeenCalled();
165
+ });
166
+
167
+ it("renders no per-cell pressable chrome inside body cells", () => {
168
+ const { container } = renderGrid();
169
+ const body = container.querySelector("tbody");
170
+ expect(body).not.toBeNull();
171
+ // Link cells are the only sanctioned inner interactive content; no
172
+ // per-cell buttons wrap plain values.
173
+ expect(body!.querySelectorAll('[role="button"], button')).toHaveLength(0);
174
+ });
175
+ });