@multiplatform.one/frappe-ui 7.28.1 → 7.29.1

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,617 @@
1
+ // Pure model for the doctype REPORT view (desk report-builder parity), the
2
+ // one FrappeReportBuilder renders and the desk's view=report shows
3
+ // (MPO-15, GL-28). UI-free on purpose: default-column derivation (desk set_default_fields),
4
+ // the per-column filter-row → frappe-tuple mapping (list filter-fields
5
+ // vocabulary), desk `report_settings` JSON round-trip (saved Report docs
6
+ // desk can open and vice versa), deterministic sort/group/totals over the
7
+ // live collection rows, and CSV assembly for export.
8
+ import { fieldKind, selectOptionsFromField, type FilterFieldKind } from "../filterFields";
9
+ import type { DocFieldMeta, DoctypeMeta } from "../formEngine/doctypeMeta";
10
+ import type { FrappeFieldFilter } from "../useConnectionFilters";
11
+
12
+ type FrappeFilter = FrappeFieldFilter;
13
+ type FrappeFilters = FrappeFieldFilter[];
14
+
15
+ /** The report's filter tuples: `[fieldname, operator, value]`. */
16
+ export type ReportFilters = FrappeFilters;
17
+
18
+ /**
19
+ * What the report view reads off a doctype's meta. Only `fields` is
20
+ * required, so a source without DocType meta (the shape of a record the
21
+ * reader can see) stands in for it.
22
+ */
23
+ export type ReportMetaSource = Pick<DoctypeMeta, "fields"> &
24
+ Partial<Pick<DoctypeMeta, "title_field" | "image_field" | "sort_field" | "sort_order">>;
25
+
26
+ // ── Column model ──────────────────────────────────────────────
27
+
28
+ export interface ReportColumnDef {
29
+ fieldname: string;
30
+ label: string;
31
+ fieldtype: string;
32
+ kind: FilterFieldKind;
33
+ /** Link target doctype or raw Select options string. */
34
+ options?: string;
35
+ /** Fixed column width (px) — one scrollport, so widths align all rows. */
36
+ width: number;
37
+ align: "left" | "right" | "center";
38
+ }
39
+
40
+ export type ReportRow = { name: string } & Record<string, unknown>;
41
+
42
+ export interface ReportSortState {
43
+ field: string;
44
+ order: "asc" | "desc";
45
+ }
46
+
47
+ /** Field types that can render/sort as report cells (desk is_value_type). */
48
+ const VALUELESS_FIELDTYPES = new Set([
49
+ "Section Break",
50
+ "Column Break",
51
+ "Tab Break",
52
+ "HTML",
53
+ "Table",
54
+ "Table MultiSelect",
55
+ "Button",
56
+ "Image",
57
+ "Fold",
58
+ "Heading",
59
+ ]);
60
+
61
+ export const REPORT_NUMERIC_FIELDTYPES = new Set([
62
+ "Int",
63
+ "Float",
64
+ "Currency",
65
+ "Percent",
66
+ "Rating",
67
+ "Duration",
68
+ ]);
69
+
70
+ const WIDE_FIELDTYPES = new Set([
71
+ "Small Text",
72
+ "Text",
73
+ "Long Text",
74
+ "Text Editor",
75
+ "Markdown Editor",
76
+ "Code",
77
+ "JSON",
78
+ ]);
79
+
80
+ /** Desk report page lengths. */
81
+ export const REPORT_PAGE_SIZES = [20, 100, 500] as const;
82
+ export const DEFAULT_REPORT_PAGE_SIZE: number = REPORT_PAGE_SIZES[0];
83
+
84
+ export function defaultColumnWidth(fieldtype: string): number {
85
+ if (fieldtype === "Check") return 90;
86
+ if (REPORT_NUMERIC_FIELDTYPES.has(fieldtype)) return 110;
87
+ if (fieldtype === "Date") return 120;
88
+ if (fieldtype === "Datetime") return 165;
89
+ if (WIDE_FIELDTYPES.has(fieldtype)) return 240;
90
+ return 160;
91
+ }
92
+
93
+ export function columnAlign(fieldtype: string): ReportColumnDef["align"] {
94
+ if (fieldtype === "Check") return "center";
95
+ return REPORT_NUMERIC_FIELDTYPES.has(fieldtype) ? "right" : "left";
96
+ }
97
+
98
+ function toColumnDef(field: DocFieldMeta): ReportColumnDef {
99
+ const fieldtype = String(field.fieldtype);
100
+ return {
101
+ fieldname: field.fieldname,
102
+ label: field.label || field.fieldname,
103
+ fieldtype,
104
+ kind: fieldKind(fieldtype),
105
+ options: field.options,
106
+ width: defaultColumnWidth(fieldtype),
107
+ align: columnAlign(fieldtype),
108
+ };
109
+ }
110
+
111
+ /** Standard doc fields every doctype carries (absent from meta.fields). */
112
+ function standardReportColumns(): ReportColumnDef[] {
113
+ return [
114
+ {
115
+ fieldname: "name",
116
+ label: "ID",
117
+ fieldtype: "Data",
118
+ kind: "text",
119
+ width: 160,
120
+ align: "left",
121
+ },
122
+ {
123
+ fieldname: "owner",
124
+ label: "Created By",
125
+ fieldtype: "Link",
126
+ kind: "link",
127
+ options: "User",
128
+ width: 160,
129
+ align: "left",
130
+ },
131
+ {
132
+ fieldname: "creation",
133
+ label: "Created On",
134
+ fieldtype: "Datetime",
135
+ kind: "date",
136
+ width: 165,
137
+ align: "left",
138
+ },
139
+ {
140
+ fieldname: "modified",
141
+ label: "Last Updated On",
142
+ fieldtype: "Datetime",
143
+ kind: "date",
144
+ width: 165,
145
+ align: "left",
146
+ },
147
+ {
148
+ fieldname: "modified_by",
149
+ label: "Last Updated By",
150
+ fieldtype: "Link",
151
+ kind: "link",
152
+ options: "User",
153
+ width: 160,
154
+ align: "left",
155
+ },
156
+ ];
157
+ }
158
+
159
+ /**
160
+ * Every column the picker can offer: ID + meta value-type data fields (in
161
+ * meta order, hidden included — desk's picker offers them too) + the
162
+ * remaining standard doc fields.
163
+ */
164
+ export function reportColumnsFromMeta(meta?: ReportMetaSource): ReportColumnDef[] {
165
+ const out: ReportColumnDef[] = [];
166
+ const seen = new Set<string>();
167
+ const push = (column: ReportColumnDef) => {
168
+ if (!column.fieldname || seen.has(column.fieldname)) return;
169
+ seen.add(column.fieldname);
170
+ out.push(column);
171
+ };
172
+ push(standardReportColumns()[0]); // ID first, desk parity
173
+ for (const field of meta?.fields ?? []) {
174
+ const fieldtype = String(field.fieldtype);
175
+ if (VALUELESS_FIELDTYPES.has(fieldtype) || fieldtype.startsWith("Table")) continue;
176
+ push(toColumnDef(field));
177
+ }
178
+ for (const standard of standardReportColumns()) push(standard);
179
+ return out;
180
+ }
181
+
182
+ /**
183
+ * Desk default report columns (report_view.js set_default_fields):
184
+ * name, a `status` docfield when the doctype has one (nearest meta-derivable
185
+ * stand-in for desk's computed Status indicator), title_field + image_field,
186
+ * fields flagged in_list_view OR in_standard_filter (value-type, not
187
+ * report_hide), currency-source fields referenced by flagged Currency
188
+ * columns, then modified (a fresh report should carry recency). Desk's
189
+ * listview_settings.add_fields (e.g. User's "enabled") live in desk-side JS
190
+ * bundles and cannot be derived from the meta channel — honest subset.
191
+ */
192
+ export function defaultReportFieldnames(meta?: ReportMetaSource): string[] {
193
+ const out: string[] = [];
194
+ const seen = new Set<string>();
195
+ const push = (fieldname?: string) => {
196
+ if (!fieldname || seen.has(fieldname)) return;
197
+ seen.add(fieldname);
198
+ out.push(fieldname);
199
+ };
200
+ const pickable = (field: DocFieldMeta): boolean => {
201
+ const fieldtype = String(field.fieldtype);
202
+ return (
203
+ !VALUELESS_FIELDTYPES.has(fieldtype) &&
204
+ !fieldtype.startsWith("Table") &&
205
+ field.report_hide !== 1
206
+ );
207
+ };
208
+ const fields = meta?.fields ?? [];
209
+ push("name");
210
+ const status = fields.find((field) => field.fieldname === "status");
211
+ if (status && pickable(status)) push("status");
212
+ push(meta?.title_field);
213
+ push(meta?.image_field);
214
+ const flagged = fields.filter(
215
+ (field) => pickable(field) && (field.in_list_view === 1 || field.in_standard_filter === 1),
216
+ );
217
+ for (const field of flagged) push(field.fieldname);
218
+ // Desk: a flagged Currency column pulls in the field naming its currency.
219
+ for (const field of flagged) {
220
+ if (String(field.fieldtype) !== "Currency" || !field.options) continue;
221
+ push(field.options.includes(":") ? field.options.split(":")[1] : field.options);
222
+ }
223
+ push("modified");
224
+ return out;
225
+ }
226
+
227
+ /** Resolve selected fieldnames against the pickable set (drops unknowns). */
228
+ export function resolveSelectedColumns(
229
+ all: ReportColumnDef[],
230
+ fieldnames: readonly string[],
231
+ ): ReportColumnDef[] {
232
+ const byName = new Map(all.map((column) => [column.fieldname, column]));
233
+ const out: ReportColumnDef[] = [];
234
+ for (const fieldname of fieldnames) {
235
+ const column = byName.get(fieldname);
236
+ if (column) out.push(column);
237
+ }
238
+ return out;
239
+ }
240
+
241
+ // ── Per-column filter row → frappe tuples ─────────────────────
242
+
243
+ const COMPARISON_PREFIXES = [">=", "<=", "!=", ">", "<", "="] as const;
244
+
245
+ const CHECK_TRUE = new Set(["1", "true", "yes", "y", "✓"]);
246
+ const CHECK_FALSE = new Set(["0", "false", "no", "n"]);
247
+
248
+ /**
249
+ * One filter-row input → one frappe tuple (null = no filter yet).
250
+ * Mapping mirrors the list filter vocabulary (filter-fields):
251
+ * text / link → `like %value%` (contains; user-typed % wins)
252
+ * select → `=` on an exact option match (case-insensitive),
253
+ * else `like %value%` so partial typing still narrows
254
+ * check → yes/no/1/0/true/false → `= 1|0`
255
+ * number/date → optional comparison prefix (`>`, `<`, `>=`, `<=`, `!=`,
256
+ * `=`), bare values equal-match (desk operator-hint parity)
257
+ */
258
+ export function columnFilterTuple(column: ReportColumnDef, raw: string): FrappeFilter | null {
259
+ const text = raw.trim();
260
+ if (!text) return null;
261
+ switch (column.kind) {
262
+ case "check": {
263
+ const lowered = text.toLowerCase();
264
+ if (CHECK_TRUE.has(lowered)) return [column.fieldname, "=", 1];
265
+ if (CHECK_FALSE.has(lowered)) return [column.fieldname, "=", 0];
266
+ return null;
267
+ }
268
+ case "select": {
269
+ const match = selectOptionsFromField(column.options).find(
270
+ (option) => option.value.toLowerCase() === text.toLowerCase(),
271
+ );
272
+ if (match) return [column.fieldname, "=", match.value];
273
+ return [column.fieldname, "like", text.includes("%") ? text : `%${text}%`];
274
+ }
275
+ case "number":
276
+ case "date": {
277
+ for (const prefix of COMPARISON_PREFIXES) {
278
+ if (text.startsWith(prefix)) {
279
+ const value = text.slice(prefix.length).trim();
280
+ return value ? [column.fieldname, prefix, value] : null;
281
+ }
282
+ }
283
+ return [column.fieldname, "=", text];
284
+ }
285
+ default:
286
+ return [column.fieldname, "like", text.includes("%") ? text : `%${text}%`];
287
+ }
288
+ }
289
+
290
+ /** All committed filter-row inputs → tuples (unparseable inputs skipped). */
291
+ export function columnFilterTuples(
292
+ columns: readonly ReportColumnDef[],
293
+ filters: Readonly<Record<string, string>>,
294
+ ): FrappeFilters {
295
+ const out: FrappeFilters = [];
296
+ for (const column of columns) {
297
+ const raw = filters[column.fieldname];
298
+ if (raw === undefined) continue;
299
+ const tuple = columnFilterTuple(column, raw);
300
+ if (tuple) out.push(tuple);
301
+ }
302
+ return out;
303
+ }
304
+
305
+ // ── `?f=` deep-link param ─────────────────────────────────────
306
+
307
+ /**
308
+ * Parse the `?f=` search param: urlencoded JSON FrappeFilters. The router
309
+ * usually decodes the param already, so the raw value is tried first
310
+ * (same contract as the list screen's parser). Malformed input degrades to
311
+ * "no filters".
312
+ */
313
+ export function parseReportFiltersParam(raw: unknown): FrappeFilters | undefined {
314
+ if (typeof raw !== "string" || !raw) return undefined;
315
+ const candidates = [raw];
316
+ try {
317
+ const decoded = decodeURIComponent(raw);
318
+ if (decoded !== raw) candidates.push(decoded);
319
+ } catch {
320
+ // raw is not percent-encoded — the direct candidate covers it
321
+ }
322
+ for (const candidate of candidates) {
323
+ try {
324
+ const parsed: unknown = JSON.parse(candidate);
325
+ if (!Array.isArray(parsed)) continue;
326
+ const filters: FrappeFilters = [];
327
+ for (const tuple of parsed) {
328
+ if (
329
+ Array.isArray(tuple) &&
330
+ tuple.length === 3 &&
331
+ typeof tuple[0] === "string" &&
332
+ typeof tuple[1] === "string"
333
+ ) {
334
+ filters.push([tuple[0], tuple[1], tuple[2]]);
335
+ }
336
+ }
337
+ if (filters.length > 0) return filters;
338
+ } catch {
339
+ // not JSON — try the next candidate
340
+ }
341
+ }
342
+ return undefined;
343
+ }
344
+
345
+ // ── Desk saved-report round trip ──────────────────────────────
346
+ // Report docs store json = { fields: [fieldname, doctype][], filters:
347
+ // [doctype, fieldname, operator, value][], order_by: "`tabX`.`field` desc",
348
+ // add_totals_row, page_length, group_by, column_widths, ... }. Serialize
349
+ // the same shape so desk opens our reports and we open desk's.
350
+
351
+ export interface ReportViewConfig {
352
+ fieldnames: string[];
353
+ filters: FrappeFilters;
354
+ sort?: ReportSortState;
355
+ pageSize?: number;
356
+ groupBy?: string | null;
357
+ addTotals?: boolean;
358
+ }
359
+
360
+ interface DeskGroupBySettings {
361
+ group_by?: string | null;
362
+ aggregate_function?: string;
363
+ aggregate_on?: string | null;
364
+ }
365
+
366
+ export interface DeskReportSettings {
367
+ fields: Array<[string, string]>;
368
+ filters: Array<[string, string, string, unknown]>;
369
+ order_by: string;
370
+ add_totals_row: 0 | 1;
371
+ page_length: number;
372
+ column_widths: Record<string, number>;
373
+ group_by?: DeskGroupBySettings | null;
374
+ }
375
+
376
+ const BACKTICK_FIELD_RE = /`tab([^`]+)`\.`([^`]+)`/;
377
+
378
+ function toDeskFieldExpr(doctype: string, fieldname: string): string {
379
+ return `\`tab${doctype}\`.\`${fieldname}\``;
380
+ }
381
+
382
+ /** Parse "`tabUser`.`modified` desc" (or bare "modified desc"). */
383
+ export function parseDeskOrderBy(orderBy: unknown): ReportSortState | undefined {
384
+ if (typeof orderBy !== "string" || !orderBy.trim()) return undefined;
385
+ const [expr, direction] = orderBy.trim().split(/\s+/);
386
+ const backticked = expr.match(BACKTICK_FIELD_RE);
387
+ const field = backticked ? backticked[2] : expr.replace(/`/g, "");
388
+ if (!field) return undefined;
389
+ return { field, order: direction?.toLowerCase() === "asc" ? "asc" : "desc" };
390
+ }
391
+
392
+ export function toDeskReportSettings(
393
+ doctype: string,
394
+ config: ReportViewConfig,
395
+ ): DeskReportSettings {
396
+ const sort = config.sort ?? { field: "modified", order: "desc" };
397
+ return {
398
+ fields: config.fieldnames.map((fieldname) => [fieldname, doctype]),
399
+ filters: config.filters.map(([fieldname, operator, value]) => [
400
+ doctype,
401
+ fieldname,
402
+ operator,
403
+ value,
404
+ ]),
405
+ order_by: `${toDeskFieldExpr(doctype, sort.field)} ${sort.order}`,
406
+ add_totals_row: config.addTotals ? 1 : 0,
407
+ page_length: config.pageSize ?? DEFAULT_REPORT_PAGE_SIZE,
408
+ column_widths: {},
409
+ group_by: config.groupBy
410
+ ? {
411
+ group_by: toDeskFieldExpr(doctype, config.groupBy),
412
+ aggregate_function: "count",
413
+ aggregate_on: null,
414
+ }
415
+ : null,
416
+ };
417
+ }
418
+
419
+ /**
420
+ * Desk Report json → view config. Child-doctype fields/filters are dropped
421
+ * (this view renders parent fields only — honest subset), legacy plain
422
+ * string field entries are honored.
423
+ */
424
+ export function fromDeskReportSettings(
425
+ doctype: string,
426
+ json: unknown,
427
+ ): ReportViewConfig | undefined {
428
+ if (!json || typeof json !== "object") return undefined;
429
+ const raw = json as Record<string, unknown>;
430
+ const fieldnames: string[] = [];
431
+ if (Array.isArray(raw.fields)) {
432
+ for (const entry of raw.fields) {
433
+ if (typeof entry === "string") {
434
+ fieldnames.push(entry);
435
+ } else if (Array.isArray(entry) && typeof entry[0] === "string") {
436
+ const fieldDoctype = typeof entry[1] === "string" ? entry[1] : doctype;
437
+ if (fieldDoctype === doctype) fieldnames.push(entry[0]);
438
+ }
439
+ }
440
+ }
441
+ const filters: FrappeFilters = [];
442
+ if (Array.isArray(raw.filters)) {
443
+ for (const entry of raw.filters) {
444
+ if (!Array.isArray(entry)) continue;
445
+ if (entry.length === 4) {
446
+ const [filterDoctype, fieldname, operator, value] = entry;
447
+ if (
448
+ filterDoctype === doctype &&
449
+ typeof fieldname === "string" &&
450
+ typeof operator === "string"
451
+ ) {
452
+ filters.push([fieldname, operator, value]);
453
+ }
454
+ } else if (
455
+ entry.length === 3 &&
456
+ typeof entry[0] === "string" &&
457
+ typeof entry[1] === "string"
458
+ ) {
459
+ filters.push([entry[0], entry[1], entry[2]]);
460
+ }
461
+ }
462
+ }
463
+ const groupByRaw = raw.group_by as DeskGroupBySettings | null | undefined;
464
+ const groupByExpr = typeof groupByRaw?.group_by === "string" ? groupByRaw.group_by : undefined;
465
+ const groupByMatch = groupByExpr?.match(BACKTICK_FIELD_RE);
466
+ return {
467
+ fieldnames,
468
+ filters,
469
+ sort: parseDeskOrderBy(raw.order_by),
470
+ pageSize:
471
+ typeof raw.page_length === "number" && raw.page_length > 0 ? raw.page_length : undefined,
472
+ groupBy: groupByMatch ? groupByMatch[2] : null,
473
+ addTotals: raw.add_totals_row === 1,
474
+ };
475
+ }
476
+
477
+ // ── Sorting, grouping, totals ─────────────────────────────────
478
+
479
+ function isNullish(value: unknown): value is null | undefined {
480
+ return value === null || value === undefined;
481
+ }
482
+
483
+ export function compareCellValues(a: unknown, b: unknown): number {
484
+ if (isNullish(a) && isNullish(b)) return 0;
485
+ if (isNullish(a)) return 1;
486
+ if (isNullish(b)) return -1;
487
+ if (typeof a === "number" && typeof b === "number") return a - b;
488
+ const left = String(a);
489
+ const right = String(b);
490
+ return left < right ? -1 : left > right ? 1 : 0;
491
+ }
492
+
493
+ /**
494
+ * Deterministic client ordering of the live collection rows (live-query row
495
+ * order is not guaranteed): group field first (when grouping), then the
496
+ * sort field in its direction, name as the final tiebreak.
497
+ */
498
+ export function orderReportRows(
499
+ rows: readonly ReportRow[],
500
+ sort: ReportSortState,
501
+ groupBy?: string | null,
502
+ ): ReportRow[] {
503
+ const direction = sort.order === "asc" ? 1 : -1;
504
+ const sorted = [...rows];
505
+ sorted.sort((a, b) => {
506
+ if (groupBy) {
507
+ const grouped = compareCellValues(a[groupBy], b[groupBy]);
508
+ if (grouped !== 0) return grouped;
509
+ }
510
+ const left = a[sort.field];
511
+ const right = b[sort.field];
512
+ const ordered =
513
+ isNullish(left) || isNullish(right)
514
+ ? compareCellValues(left, right)
515
+ : direction * compareCellValues(left, right);
516
+ return ordered || compareCellValues(a.name, b.name);
517
+ });
518
+ return sorted;
519
+ }
520
+
521
+ export interface ReportGroupHeader {
522
+ kind: "group";
523
+ key: string;
524
+ value: unknown;
525
+ /** Rows carrying this value across the FULL filtered result. */
526
+ count: number;
527
+ }
528
+
529
+ export interface ReportDataRow {
530
+ kind: "row";
531
+ key: string;
532
+ row: ReportRow;
533
+ /** 1-based position in the full ordered result (row-number column). */
534
+ index: number;
535
+ }
536
+
537
+ export type ReportGridRow = ReportGroupHeader | ReportDataRow;
538
+
539
+ /**
540
+ * Page slice → grid rows. With grouping active, group headers are injected
541
+ * at value boundaries inside the page; counts cover the full ordered result
542
+ * so a group spanning pages reads honestly.
543
+ */
544
+ export function buildGridRows(
545
+ orderedRows: readonly ReportRow[],
546
+ pageStart: number,
547
+ pageSize: number,
548
+ groupBy?: string | null,
549
+ ): ReportGridRow[] {
550
+ const page = orderedRows.slice(pageStart, pageStart + pageSize);
551
+ const out: ReportGridRow[] = [];
552
+ let previousGroup: string | undefined;
553
+ const counts = new Map<string, number>();
554
+ if (groupBy) {
555
+ for (const row of orderedRows) {
556
+ const groupKey = String(row[groupBy] ?? "");
557
+ counts.set(groupKey, (counts.get(groupKey) ?? 0) + 1);
558
+ }
559
+ }
560
+ page.forEach((row, index) => {
561
+ if (groupBy) {
562
+ const groupKey = String(row[groupBy] ?? "");
563
+ if (previousGroup === undefined || groupKey !== previousGroup) {
564
+ previousGroup = groupKey;
565
+ out.push({
566
+ kind: "group",
567
+ key: `group:${groupKey}:${pageStart + index}`,
568
+ value: row[groupBy],
569
+ count: counts.get(groupKey) ?? 0,
570
+ });
571
+ }
572
+ }
573
+ out.push({ kind: "row", key: row.name, row, index: pageStart + index + 1 });
574
+ });
575
+ return out;
576
+ }
577
+
578
+ export interface ReportTotalsCell {
579
+ fieldname: string;
580
+ fieldtype: string;
581
+ value: number;
582
+ }
583
+
584
+ /** Sums per visible numeric column over the FULL filtered result. */
585
+ export function computeTotals(
586
+ columns: readonly ReportColumnDef[],
587
+ rows: readonly ReportRow[],
588
+ ): ReportTotalsCell[] {
589
+ if (rows.length === 0) return [];
590
+ return columns
591
+ .filter((column) => REPORT_NUMERIC_FIELDTYPES.has(column.fieldtype))
592
+ .map((column) => ({
593
+ fieldname: column.fieldname,
594
+ fieldtype: column.fieldtype,
595
+ value: rows.reduce((sum, row) => sum + (Number(row[column.fieldname]) || 0), 0),
596
+ }));
597
+ }
598
+
599
+ // ── CSV export ────────────────────────────────────────────────
600
+
601
+ function escapeCsvCell(value: unknown): string {
602
+ if (value === null || value === undefined) return "";
603
+ const text = String(value);
604
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
605
+ }
606
+
607
+ /** CSV over the FULL filtered + ordered result (not just the page). */
608
+ export function buildReportCsv(
609
+ columns: readonly ReportColumnDef[],
610
+ rows: readonly ReportRow[],
611
+ ): string {
612
+ const lines = [
613
+ columns.map((column) => escapeCsvCell(column.label)).join(","),
614
+ ...rows.map((row) => columns.map((column) => escapeCsvCell(row[column.fieldname])).join(",")),
615
+ ];
616
+ return lines.join("\n");
617
+ }