@multiplatform.one/backoffice 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 (269) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/desk/DeskSurface.native.js.map +1 -1
  3. package/dist/cjs/form/DocFormScreen.cjs +167 -224
  4. package/dist/cjs/form/DocFormScreen.native.js +182 -259
  5. package/dist/cjs/form/DocFormScreen.native.js.map +1 -1
  6. package/dist/cjs/i18n/en.cjs +1 -0
  7. package/dist/cjs/i18n/en.native.js +1 -0
  8. package/dist/cjs/i18n/en.native.js.map +1 -1
  9. package/dist/cjs/list/ListBulkActionsBar.cjs +30 -18
  10. package/dist/cjs/list/ListBulkActionsBar.native.js +43 -31
  11. package/dist/cjs/list/ListBulkActionsBar.native.js.map +1 -1
  12. package/dist/cjs/list/sidebar-section.cjs +15 -7
  13. package/dist/cjs/list/sidebar-section.native.js +16 -7
  14. package/dist/cjs/list/sidebar-section.native.js.map +1 -1
  15. package/dist/cjs/report/DoctypeReportView.cjs +20 -374
  16. package/dist/cjs/report/DoctypeReportView.native.js +23 -455
  17. package/dist/cjs/report/DoctypeReportView.native.js.map +1 -1
  18. package/dist/cjs/report/QueryReportScreen.cjs +33 -23
  19. package/dist/cjs/report/QueryReportScreen.native.js +33 -23
  20. package/dist/cjs/report/QueryReportScreen.native.js.map +1 -1
  21. package/dist/cjs/report/queryReportColumns.cjs +3 -105
  22. package/dist/cjs/report/queryReportColumns.native.js +2 -129
  23. package/dist/cjs/report/queryReportColumns.native.js.map +1 -1
  24. package/dist/cjs/report/queryReportFilterBar.cjs +3 -4
  25. package/dist/cjs/report/queryReportFilterBar.native.js +3 -4
  26. package/dist/cjs/report/queryReportFilterBar.native.js.map +1 -1
  27. package/dist/cjs/report/queryReportUrlState.cjs +2 -2
  28. package/dist/cjs/report/queryReportUrlState.native.js +2 -2
  29. package/dist/cjs/report/queryReportUrlState.native.js.map +1 -1
  30. package/dist/cjs/report/report-grid.cjs +2 -426
  31. package/dist/cjs/report/report-grid.native.js +1 -479
  32. package/dist/cjs/report/report-grid.native.js.map +1 -1
  33. package/dist/cjs/report/report-toolbar.cjs +2 -228
  34. package/dist/cjs/report/report-toolbar.native.js +1 -274
  35. package/dist/cjs/report/report-toolbar.native.js.map +1 -1
  36. package/dist/cjs/report/reportShapes.cjs +12 -383
  37. package/dist/cjs/report/reportShapes.native.js +12 -501
  38. package/dist/cjs/report/reportShapes.native.js.map +1 -1
  39. package/dist/cjs/report/reportViewModel.cjs +20 -360
  40. package/dist/cjs/report/reportViewModel.native.js +19 -627
  41. package/dist/cjs/report/reportViewModel.native.js.map +1 -1
  42. package/dist/cjs/report/savedReports.cjs +2 -106
  43. package/dist/cjs/report/savedReports.native.js +1 -132
  44. package/dist/cjs/report/savedReports.native.js.map +1 -1
  45. package/dist/cjs/report/useQueryReport.cjs +11 -219
  46. package/dist/cjs/report/useQueryReport.native.js +11 -244
  47. package/dist/cjs/report/useQueryReport.native.js.map +1 -1
  48. package/dist/cjs/shared/measures.cjs +3 -5
  49. package/dist/cjs/shared/measures.native.js +3 -5
  50. package/dist/cjs/shared/measures.native.js.map +1 -1
  51. package/dist/cjs/views/ReportView.cjs +3 -256
  52. package/dist/cjs/views/ReportView.native.js +3 -382
  53. package/dist/cjs/views/ReportView.native.js.map +1 -1
  54. package/dist/cjs/views/report-controls.cjs +6 -6
  55. package/dist/cjs/views/report-controls.native.js +6 -6
  56. package/dist/cjs/views/report-controls.native.js.map +1 -1
  57. package/dist/esm/desk/DeskSurface.mjs.map +1 -1
  58. package/dist/esm/desk/DeskSurface.native.js.map +1 -1
  59. package/dist/esm/form/DocFormScreen.mjs +169 -226
  60. package/dist/esm/form/DocFormScreen.mjs.map +1 -1
  61. package/dist/esm/form/DocFormScreen.native.js +184 -261
  62. package/dist/esm/form/DocFormScreen.native.js.map +1 -1
  63. package/dist/esm/i18n/en.mjs +1 -0
  64. package/dist/esm/i18n/en.mjs.map +1 -1
  65. package/dist/esm/i18n/en.native.js +1 -0
  66. package/dist/esm/i18n/en.native.js.map +1 -1
  67. package/dist/esm/list/ListBulkActionsBar.mjs +31 -19
  68. package/dist/esm/list/ListBulkActionsBar.mjs.map +1 -1
  69. package/dist/esm/list/ListBulkActionsBar.native.js +37 -25
  70. package/dist/esm/list/ListBulkActionsBar.native.js.map +1 -1
  71. package/dist/esm/list/sidebar-section.mjs +16 -8
  72. package/dist/esm/list/sidebar-section.mjs.map +1 -1
  73. package/dist/esm/list/sidebar-section.native.js +17 -8
  74. package/dist/esm/list/sidebar-section.native.js.map +1 -1
  75. package/dist/esm/report/DoctypeReportView.mjs +21 -375
  76. package/dist/esm/report/DoctypeReportView.mjs.map +1 -1
  77. package/dist/esm/report/DoctypeReportView.native.js +24 -456
  78. package/dist/esm/report/DoctypeReportView.native.js.map +1 -1
  79. package/dist/esm/report/QueryReportScreen.mjs +26 -16
  80. package/dist/esm/report/QueryReportScreen.mjs.map +1 -1
  81. package/dist/esm/report/QueryReportScreen.native.js +26 -16
  82. package/dist/esm/report/QueryReportScreen.native.js.map +1 -1
  83. package/dist/esm/report/queryReportColumns.mjs +1 -103
  84. package/dist/esm/report/queryReportColumns.mjs.map +1 -1
  85. package/dist/esm/report/queryReportColumns.native.js +1 -117
  86. package/dist/esm/report/queryReportColumns.native.js.map +1 -1
  87. package/dist/esm/report/queryReportFilterBar.mjs +1 -2
  88. package/dist/esm/report/queryReportFilterBar.mjs.map +1 -1
  89. package/dist/esm/report/queryReportFilterBar.native.js +1 -2
  90. package/dist/esm/report/queryReportFilterBar.native.js.map +1 -1
  91. package/dist/esm/report/queryReportUrlState.mjs +1 -1
  92. package/dist/esm/report/queryReportUrlState.native.js +1 -1
  93. package/dist/esm/report/report-grid.mjs +1 -425
  94. package/dist/esm/report/report-grid.mjs.map +1 -1
  95. package/dist/esm/report/report-grid.native.js +1 -468
  96. package/dist/esm/report/report-grid.native.js.map +1 -1
  97. package/dist/esm/report/report-toolbar.mjs +2 -228
  98. package/dist/esm/report/report-toolbar.mjs.map +1 -1
  99. package/dist/esm/report/report-toolbar.native.js +2 -264
  100. package/dist/esm/report/report-toolbar.native.js.map +1 -1
  101. package/dist/esm/report/reportShapes.mjs +2 -373
  102. package/dist/esm/report/reportShapes.mjs.map +1 -1
  103. package/dist/esm/report/reportShapes.native.js +2 -491
  104. package/dist/esm/report/reportShapes.native.js.map +1 -1
  105. package/dist/esm/report/reportViewModel.mjs +2 -342
  106. package/dist/esm/report/reportViewModel.mjs.map +1 -1
  107. package/dist/esm/report/reportViewModel.native.js +2 -610
  108. package/dist/esm/report/reportViewModel.native.js.map +1 -1
  109. package/dist/esm/report/savedReports.mjs +1 -105
  110. package/dist/esm/report/savedReports.mjs.map +1 -1
  111. package/dist/esm/report/savedReports.native.js +1 -132
  112. package/dist/esm/report/savedReports.native.js.map +1 -1
  113. package/dist/esm/report/useQueryReport.mjs +2 -210
  114. package/dist/esm/report/useQueryReport.mjs.map +1 -1
  115. package/dist/esm/report/useQueryReport.native.js +2 -235
  116. package/dist/esm/report/useQueryReport.native.js.map +1 -1
  117. package/dist/esm/shared/measures.mjs +1 -3
  118. package/dist/esm/shared/measures.mjs.map +1 -1
  119. package/dist/esm/shared/measures.native.js +1 -3
  120. package/dist/esm/shared/measures.native.js.map +1 -1
  121. package/dist/esm/views/ReportView.mjs +3 -256
  122. package/dist/esm/views/ReportView.mjs.map +1 -1
  123. package/dist/esm/views/ReportView.native.js +3 -382
  124. package/dist/esm/views/ReportView.native.js.map +1 -1
  125. package/dist/esm/views/report-controls.mjs +3 -3
  126. package/dist/esm/views/report-controls.mjs.map +1 -1
  127. package/dist/esm/views/report-controls.native.js +3 -3
  128. package/dist/esm/views/report-controls.native.js.map +1 -1
  129. package/dist/jsx/desk/DeskSurface.mjs.map +1 -1
  130. package/dist/jsx/desk/DeskSurface.native.js.map +1 -1
  131. package/dist/jsx/form/DocFormScreen.mjs +169 -226
  132. package/dist/jsx/form/DocFormScreen.mjs.map +1 -1
  133. package/dist/jsx/form/DocFormScreen.native.js +182 -259
  134. package/dist/jsx/form/DocFormScreen.native.js.map +1 -1
  135. package/dist/jsx/i18n/en.mjs +1 -0
  136. package/dist/jsx/i18n/en.mjs.map +1 -1
  137. package/dist/jsx/i18n/en.native.js +1 -0
  138. package/dist/jsx/i18n/en.native.js.map +1 -1
  139. package/dist/jsx/list/ListBulkActionsBar.mjs +31 -19
  140. package/dist/jsx/list/ListBulkActionsBar.mjs.map +1 -1
  141. package/dist/jsx/list/ListBulkActionsBar.native.js +43 -31
  142. package/dist/jsx/list/ListBulkActionsBar.native.js.map +1 -1
  143. package/dist/jsx/list/sidebar-section.mjs +16 -8
  144. package/dist/jsx/list/sidebar-section.mjs.map +1 -1
  145. package/dist/jsx/list/sidebar-section.native.js +16 -7
  146. package/dist/jsx/list/sidebar-section.native.js.map +1 -1
  147. package/dist/jsx/report/DoctypeReportView.mjs +21 -375
  148. package/dist/jsx/report/DoctypeReportView.mjs.map +1 -1
  149. package/dist/jsx/report/DoctypeReportView.native.js +23 -455
  150. package/dist/jsx/report/DoctypeReportView.native.js.map +1 -1
  151. package/dist/jsx/report/QueryReportScreen.mjs +26 -16
  152. package/dist/jsx/report/QueryReportScreen.mjs.map +1 -1
  153. package/dist/jsx/report/QueryReportScreen.native.js +33 -23
  154. package/dist/jsx/report/QueryReportScreen.native.js.map +1 -1
  155. package/dist/jsx/report/queryReportColumns.mjs +1 -103
  156. package/dist/jsx/report/queryReportColumns.mjs.map +1 -1
  157. package/dist/jsx/report/queryReportColumns.native.js +2 -129
  158. package/dist/jsx/report/queryReportColumns.native.js.map +1 -1
  159. package/dist/jsx/report/queryReportFilterBar.mjs +1 -2
  160. package/dist/jsx/report/queryReportFilterBar.mjs.map +1 -1
  161. package/dist/jsx/report/queryReportFilterBar.native.js +3 -4
  162. package/dist/jsx/report/queryReportFilterBar.native.js.map +1 -1
  163. package/dist/jsx/report/queryReportUrlState.mjs +1 -1
  164. package/dist/jsx/report/queryReportUrlState.native.js +2 -2
  165. package/dist/jsx/report/queryReportUrlState.native.js.map +1 -1
  166. package/dist/jsx/report/report-grid.mjs +1 -425
  167. package/dist/jsx/report/report-grid.mjs.map +1 -1
  168. package/dist/jsx/report/report-grid.native.js +1 -479
  169. package/dist/jsx/report/report-grid.native.js.map +1 -1
  170. package/dist/jsx/report/report-toolbar.mjs +2 -228
  171. package/dist/jsx/report/report-toolbar.mjs.map +1 -1
  172. package/dist/jsx/report/report-toolbar.native.js +1 -274
  173. package/dist/jsx/report/report-toolbar.native.js.map +1 -1
  174. package/dist/jsx/report/reportShapes.mjs +2 -373
  175. package/dist/jsx/report/reportShapes.mjs.map +1 -1
  176. package/dist/jsx/report/reportShapes.native.js +12 -501
  177. package/dist/jsx/report/reportShapes.native.js.map +1 -1
  178. package/dist/jsx/report/reportViewModel.mjs +2 -342
  179. package/dist/jsx/report/reportViewModel.mjs.map +1 -1
  180. package/dist/jsx/report/reportViewModel.native.js +19 -627
  181. package/dist/jsx/report/reportViewModel.native.js.map +1 -1
  182. package/dist/jsx/report/savedReports.mjs +1 -105
  183. package/dist/jsx/report/savedReports.mjs.map +1 -1
  184. package/dist/jsx/report/savedReports.native.js +1 -132
  185. package/dist/jsx/report/savedReports.native.js.map +1 -1
  186. package/dist/jsx/report/useQueryReport.mjs +2 -210
  187. package/dist/jsx/report/useQueryReport.mjs.map +1 -1
  188. package/dist/jsx/report/useQueryReport.native.js +11 -244
  189. package/dist/jsx/report/useQueryReport.native.js.map +1 -1
  190. package/dist/jsx/shared/measures.mjs +1 -3
  191. package/dist/jsx/shared/measures.mjs.map +1 -1
  192. package/dist/jsx/shared/measures.native.js +3 -5
  193. package/dist/jsx/shared/measures.native.js.map +1 -1
  194. package/dist/jsx/views/ReportView.mjs +3 -256
  195. package/dist/jsx/views/ReportView.mjs.map +1 -1
  196. package/dist/jsx/views/ReportView.native.js +3 -382
  197. package/dist/jsx/views/ReportView.native.js.map +1 -1
  198. package/dist/jsx/views/report-controls.mjs +3 -3
  199. package/dist/jsx/views/report-controls.mjs.map +1 -1
  200. package/dist/jsx/views/report-controls.native.js +6 -6
  201. package/dist/jsx/views/report-controls.native.js.map +1 -1
  202. package/package.json +12 -12
  203. package/src/backofficeScreens.designLaw.spec.ts +0 -5
  204. package/src/desk/DeskSurface.tsx +4 -7
  205. package/src/form/DocFormScreen.chrome.spec.ts +77 -0
  206. package/src/form/DocFormScreen.guest.screen.spec.tsx +175 -0
  207. package/src/form/DocFormScreen.rights.spec.tsx +7 -3
  208. package/src/form/DocFormScreen.tsx +214 -315
  209. package/src/i18n/en.ts +1 -0
  210. package/src/list/ListBulkActionsBar.rights.spec.tsx +134 -0
  211. package/src/list/ListBulkActionsBar.screen.spec.tsx +7 -1
  212. package/src/list/ListBulkActionsBar.tsx +76 -57
  213. package/src/list/ListFilterBar.screen.spec.tsx +14 -13
  214. package/src/list/sidebar-section.tsx +11 -3
  215. package/src/report/DoctypeReportView.tsx +28 -550
  216. package/src/report/QueryReportScreen.spec.tsx +75 -1
  217. package/src/report/QueryReportScreen.tsx +38 -22
  218. package/src/report/queryReportColumns.tsx +4 -123
  219. package/src/report/queryReportFilterBar.tsx +3 -5
  220. package/src/report/queryReportUrlState.ts +1 -1
  221. package/src/report/report-grid.tsx +4 -529
  222. package/src/report/report-toolbar.tsx +4 -308
  223. package/src/report/reportShapes.ts +25 -613
  224. package/src/report/reportShim.spec.ts +162 -0
  225. package/src/report/reportViewModel.ts +32 -605
  226. package/src/report/savedReports.ts +4 -161
  227. package/src/report/useQueryReport.ts +9 -347
  228. package/src/shared/measures.ts +3 -11
  229. package/src/views/ReportView.screen.spec.tsx +25 -20
  230. package/src/views/ReportView.tsx +7 -439
  231. package/src/views/report-controls.tsx +12 -20
  232. package/types/desk/DeskSurface.d.ts.map +1 -1
  233. package/types/form/DocFormScreen.d.ts +2 -2
  234. package/types/form/DocFormScreen.d.ts.map +1 -1
  235. package/types/i18n/en.d.ts +1 -0
  236. package/types/i18n/en.d.ts.map +1 -1
  237. package/types/list/ListBulkActionsBar.d.ts.map +1 -1
  238. package/types/list/sidebar-section.d.ts.map +1 -1
  239. package/types/report/DoctypeReportView.d.ts.map +1 -1
  240. package/types/report/QueryReportScreen.d.ts +1 -1
  241. package/types/report/QueryReportScreen.d.ts.map +1 -1
  242. package/types/report/queryReportColumns.d.ts +2 -11
  243. package/types/report/queryReportColumns.d.ts.map +1 -1
  244. package/types/report/queryReportFilterBar.d.ts +1 -1
  245. package/types/report/queryReportFilterBar.d.ts.map +1 -1
  246. package/types/report/queryReportUrlState.d.ts +1 -1
  247. package/types/report/queryReportUrlState.d.ts.map +1 -1
  248. package/types/report/report-grid.d.ts +2 -19
  249. package/types/report/report-grid.d.ts.map +1 -1
  250. package/types/report/report-toolbar.d.ts +2 -29
  251. package/types/report/report-toolbar.d.ts.map +1 -1
  252. package/types/report/reportShapes.d.ts +2 -115
  253. package/types/report/reportShapes.d.ts.map +1 -1
  254. package/types/report/reportViewModel.d.ts +2 -135
  255. package/types/report/reportViewModel.d.ts.map +1 -1
  256. package/types/report/savedReports.d.ts +2 -18
  257. package/types/report/savedReports.d.ts.map +1 -1
  258. package/types/report/useQueryReport.d.ts +2 -35
  259. package/types/report/useQueryReport.d.ts.map +1 -1
  260. package/types/shared/measures.d.ts +1 -2
  261. package/types/shared/measures.d.ts.map +1 -1
  262. package/types/views/ReportView.d.ts +3 -22
  263. package/types/views/ReportView.d.ts.map +1 -1
  264. package/types/views/report-controls.d.ts +2 -5
  265. package/types/views/report-controls.d.ts.map +1 -1
  266. package/src/report/report-grid.spec.tsx +0 -175
  267. package/src/report/report-toolbar.spec.tsx +0 -90
  268. package/src/report/reportShapes.spec.ts +0 -391
  269. package/src/report/reportViewModel.spec.ts +0 -393
@@ -1,613 +1,25 @@
1
- // Pure (dependency-free) query-report wire-shape normalization — kept apart
2
- // from useQueryReport so the node-environment specs can import it without
3
- // dragging in react/frappe packages.
4
- //
5
- // PROBED response shapes (bench v15 — reports "ToDo", "Permitted Documents
6
- // For User", and the SQL query report "Backoffice QR Probe"):
7
- // POST /api/method/frappe.desk.query_report.run {report_name, filters}
8
- // → {message: {columns, result, message, chart, report_summary,
9
- // add_total_row, skip_total_row, execution_time, status}}
10
- // - columns: [{label, fieldname, fieldtype, width?, options?}] — width is
11
- // a STRING ("90"), fieldtype may be "". v15 normalizes even old-style
12
- // SQL alias columns ("Label:Fieldtype/Options:Width") into this dict
13
- // shape server-side; the string + positional-array-row forms are still
14
- // handled here defensively (older benches / custom reports).
15
- // - result: array of objects keyed by fieldname; values may embed HTML
16
- // (e.g. desk anchor tags). The totals row is appended SERVER-side when
17
- // add_total_row=1, so no client aggregation happens here.
18
- // - errors: {exc_type, _server_messages} where _server_messages is a
19
- // JSON-encoded array of JSON-encoded {message} — humanized below.
20
-
21
- export interface QueryReportColumn {
22
- fieldname: string;
23
- label: string;
24
- /** Frappe fieldtype ("Link", "Date", "Currency", … or "" when untyped). */
25
- fieldtype: string;
26
- /** Link target doctype (fieldtype "Link"). */
27
- options?: string;
28
- /** Desk pixel width hint (server sends strings like "90"). */
29
- width?: number;
30
- }
31
-
32
- export type QueryReportRow = Record<string, unknown>;
33
- export type QueryReportFilters = Record<string, unknown>;
34
-
35
- /**
36
- * One report filter's declaration — from the Report doc `filters` child
37
- * table (Report Filter rows) or extracted from the desk-side script module
38
- * (which spells mandatory as `reqd`; both normalize to `mandatory`).
39
- */
40
- export interface QueryReportFilterMeta {
41
- fieldname: string;
42
- label?: string;
43
- fieldtype?: string;
44
- options?: string;
45
- /** Script modules declare numeric/boolean defaults; doc rows are strings. */
46
- default?: string | number;
47
- mandatory?: 0 | 1;
48
- /** Desk wraps this filter's value in `%…%` before the run. */
49
- wildcard_filter?: 0 | 1;
50
- /** Applied (defaults included) but never rendered — desk parity. */
51
- hidden?: 0 | 1;
52
- /**
53
- * Desk `read_only` — the control renders (so the value is honest) but
54
- * cannot be edited. Locked filters must not open pickers (LC-21 family:
55
- * a disabled control opens nothing).
56
- */
57
- read_only?: 0 | 1;
58
- }
59
-
60
- /** Report doc bits the screen cares about (loose — server owns the schema). */
61
- export interface QueryReportMeta {
62
- name: string;
63
- report_name?: string;
64
- ref_doctype?: string;
65
- report_type?: string;
66
- disabled?: 0 | 1;
67
- prepared_report?: 0 | 1;
68
- add_total_row?: 0 | 1;
69
- filters?: QueryReportFilterMeta[];
70
- [key: string]: unknown;
71
- }
72
-
73
- /** Static data for stories/tests — skips every bench RPC. */
74
- export interface QueryReportFixture {
75
- meta?: Partial<QueryReportMeta>;
76
- /** Raw column shapes (dicts or "Label:Fieldtype/Options:Width" strings). */
77
- columns: unknown[];
78
- /** Raw rows (dicts or positional arrays). */
79
- rows: unknown[];
80
- message?: string;
81
- /** Desk-side script module text (get_script response) for filter extraction. */
82
- script?: string;
83
- }
84
-
85
- export interface QueryReportRunMessage {
86
- columns?: unknown[];
87
- result?: unknown[];
88
- message?: string | null;
89
- chart?: unknown;
90
- report_summary?: unknown;
91
- add_total_row?: 0 | 1;
92
- skip_total_row?: 0 | 1;
93
- execution_time?: number;
94
- status?: string | null;
95
- }
96
-
97
- /** frappe.scrub equivalent: "Modified On" → "modified_on". */
98
- export function scrubFieldname(label: string): string {
99
- return label
100
- .toLowerCase()
101
- .replace(/[^a-z0-9]+/g, "_")
102
- .replace(/^_+|_+$/g, "");
103
- }
104
-
105
- /** Best-effort HTML → text for report cell values and server messages. */
106
- export function stripHtml(value: string): string {
107
- if (!value.includes("<") && !value.includes("&")) return value;
108
- return value
109
- .replace(/<[^>]*>/g, "")
110
- .replace(/&nbsp;/g, " ")
111
- .replace(/&amp;/g, "&")
112
- .replace(/&lt;/g, "<")
113
- .replace(/&gt;/g, ">")
114
- .replace(/&quot;/g, '"')
115
- .replace(/&#39;/g, "'")
116
- .replace(/\s+/g, " ")
117
- .trim();
118
- }
119
-
120
- function parseWidth(raw: unknown): number | undefined {
121
- if (typeof raw === "number") return raw > 0 ? raw : undefined;
122
- if (typeof raw === "string" && raw) {
123
- const parsed = Number.parseInt(raw, 10);
124
- if (!Number.isNaN(parsed) && parsed > 0) return parsed;
125
- }
126
- return undefined;
127
- }
128
-
129
- /**
130
- * Normalize run() columns: v15 sends dicts, older/custom reports may send
131
- * "Label:Fieldtype/Options:Width" alias strings. Fieldnames are deduped so
132
- * positional rows always land somewhere addressable.
133
- */
134
- export function normalizeQueryReportColumns(
135
- raw: readonly unknown[] | undefined,
136
- ): QueryReportColumn[] {
137
- const used = new Set<string>();
138
- const columns: QueryReportColumn[] = [];
139
- (raw ?? []).forEach((entry, index) => {
140
- let label = "";
141
- let fieldname = "";
142
- let fieldtype = "";
143
- let options: string | undefined;
144
- let width: number | undefined;
145
- if (typeof entry === "string") {
146
- const [labelPart = "", typePart = "", widthPart = ""] = entry.split(":");
147
- label = labelPart.trim();
148
- const [typeBit = "", optionsBit = ""] = typePart.split("/");
149
- fieldtype = typeBit.trim();
150
- options = optionsBit.trim() || undefined;
151
- width = parseWidth(widthPart.trim());
152
- } else if (entry && typeof entry === "object") {
153
- const column = entry as Record<string, unknown>;
154
- label = typeof column.label === "string" ? column.label : "";
155
- fieldname = typeof column.fieldname === "string" ? column.fieldname : "";
156
- fieldtype = typeof column.fieldtype === "string" ? column.fieldtype : "";
157
- options = typeof column.options === "string" && column.options ? column.options : undefined;
158
- width = parseWidth(column.width);
159
- } else {
160
- return;
161
- }
162
- let id = fieldname || scrubFieldname(label) || `col_${index}`;
163
- if (used.has(id)) id = `${id}_${index}`;
164
- used.add(id);
165
- columns.push({ fieldname: id, label: label || id, fieldtype, options, width });
166
- });
167
- return columns;
168
- }
169
-
170
- /** Normalize run() rows: positional arrays are zipped onto column fieldnames. */
171
- export function normalizeQueryReportRows(
172
- raw: readonly unknown[] | undefined,
173
- columns: readonly QueryReportColumn[],
174
- ): QueryReportRow[] {
175
- const rows: QueryReportRow[] = [];
176
- for (const entry of raw ?? []) {
177
- if (Array.isArray(entry)) {
178
- const row: QueryReportRow = {};
179
- columns.forEach((column, index) => {
180
- row[column.fieldname] = entry[index];
181
- });
182
- rows.push(row);
183
- } else if (entry && typeof entry === "object") {
184
- rows.push(entry as QueryReportRow);
185
- }
186
- }
187
- return rows;
188
- }
189
-
190
- /** Truthy check/flag coercion (accepts 1, "1", true — the wire variants). */
191
- function coerceFlag(raw: unknown): 0 | 1 {
192
- return raw === 1 || raw === "1" || raw === true ? 1 : 0;
193
- }
194
-
195
- /**
196
- * One filter declaration record (a Report Filter doc row or a script-object
197
- * extraction) → normalized meta. Script modules spell mandatory as `reqd`
198
- * and may declare numeric/boolean defaults.
199
- */
200
- function coerceFilterMetaEntry(row: Record<string, unknown>): QueryReportFilterMeta | undefined {
201
- const fieldname =
202
- typeof row.fieldname === "string" && row.fieldname
203
- ? row.fieldname
204
- : typeof row.label === "string" && row.label
205
- ? scrubFieldname(row.label)
206
- : "";
207
- if (!fieldname) return undefined;
208
- let defaultValue: string | number | undefined;
209
- if (typeof row.default === "string" && row.default !== "") defaultValue = row.default;
210
- else if (typeof row.default === "number") defaultValue = row.default;
211
- else if (typeof row.default === "boolean") defaultValue = row.default ? 1 : 0;
212
- return {
213
- fieldname,
214
- label: typeof row.label === "string" ? row.label : undefined,
215
- fieldtype: typeof row.fieldtype === "string" ? row.fieldtype : undefined,
216
- options:
217
- typeof row.options === "string"
218
- ? row.options
219
- : Array.isArray(row.options) && row.options.every((entry) => typeof entry === "string")
220
- ? row.options.join("\n")
221
- : undefined,
222
- default: defaultValue,
223
- mandatory: coerceFlag(row.mandatory) || coerceFlag(row.reqd) ? 1 : 0,
224
- wildcard_filter: coerceFlag(row.wildcard_filter),
225
- hidden: coerceFlag(row.hidden),
226
- read_only: coerceFlag(row.read_only) || coerceFlag(row.readOnly) ? 1 : 0,
227
- };
228
- }
229
-
230
- export function normalizeFilterMeta(raw: unknown): QueryReportFilterMeta[] {
231
- if (!Array.isArray(raw)) return [];
232
- const filters: QueryReportFilterMeta[] = [];
233
- for (const entry of raw) {
234
- if (!entry || typeof entry !== "object") continue;
235
- const meta = coerceFilterMetaEntry(entry as Record<string, unknown>);
236
- if (meta) filters.push(meta);
237
- }
238
- return filters;
239
- }
240
-
241
- export function isEmptyFilterValue(value: unknown): boolean {
242
- return value === undefined || value === null || value === "";
243
- }
244
-
245
- /**
246
- * Draft/applied values → the dict that round-trips through `?filters=` and
247
- * `query_report.run`. Empty values drop; hidden filters keep their values
248
- * so a clear-all on the visible bar does not strip seeded hidden defaults.
249
- */
250
- export function compactQueryReportFilters(
251
- filterMeta: readonly QueryReportFilterMeta[],
252
- values: QueryReportFilters,
253
- ): QueryReportFilters {
254
- const next: QueryReportFilters = {};
255
- for (const filter of filterMeta) {
256
- const value = values[filter.fieldname];
257
- if (!isEmptyFilterValue(value)) next[filter.fieldname] = value;
258
- }
259
- return next;
260
- }
261
-
262
- /** Seedable default values ("" defaults are non-values, desk parity). */
263
- export function defaultFilterValues(
264
- filterMeta: readonly QueryReportFilterMeta[],
265
- ): QueryReportFilters {
266
- const defaults: QueryReportFilters = {};
267
- for (const filter of filterMeta) {
268
- if (filter.default !== undefined && filter.default !== "")
269
- defaults[filter.fieldname] = filter.default;
270
- }
271
- return defaults;
272
- }
273
-
274
- /**
275
- * Filter values → the `filters` dict for frappe.desk.query_report.run.
276
- * Desk parity (query_report.js get_filter_values): falsy values are OMITTED
277
- * (an unchecked Check or empty input sends nothing), `wildcard_filter`
278
- * values wrap in `%…%` unless the user typed their own wildcard, and
279
- * numeric fieldtypes send numbers (script reports do arithmetic on them).
280
- */
281
- export function buildRunFilters(
282
- filterMeta: readonly QueryReportFilterMeta[],
283
- filters: QueryReportFilters,
284
- ): QueryReportFilters {
285
- const metaByField = new Map(filterMeta.map((filter) => [filter.fieldname, filter]));
286
- const out: QueryReportFilters = {};
287
- for (const [fieldname, rawValue] of Object.entries(filters)) {
288
- let value: unknown = rawValue === true ? 1 : rawValue;
289
- if (isEmptyFilterValue(value) || value === false || value === 0) continue;
290
- const meta = metaByField.get(fieldname);
291
- if (meta?.wildcard_filter === 1 && typeof value === "string" && !value.includes("%"))
292
- value = `%${value}%`;
293
- if (
294
- meta?.fieldtype !== undefined &&
295
- NUMERIC_FILTER_FIELDTYPES.has(meta.fieldtype) &&
296
- typeof value === "string" &&
297
- value.trim() !== "" &&
298
- Number.isFinite(Number(value))
299
- )
300
- value = Number(value);
301
- out[fieldname] = value;
302
- }
303
- return out;
304
- }
305
-
306
- // ── Script-declared filters (desk-side report JS modules) ───────
307
- //
308
- // Standard reports declare filters in their desk JS module, reachable only
309
- // as raw text via frappe.desk.query_report.get_script:
310
- // frappe.query_reports["<name>"] = { filters: [{fieldname, label: __("…"),
311
- // fieldtype, options, reqd, default, get_query() {…}}, …] }
312
- // The declarative subset (string/number/boolean values, __() labels, string
313
- // arrays) is extracted WITHOUT eval by a small string/comment-aware scanner;
314
- // function-valued keys (get_query, on_change, formatters) are skipped.
315
- // Entries that don't yield a renderable control count toward `declaredCount`
316
- // so the surface can render an honest "defined in code" gap note.
317
-
318
- export interface ScriptFilterExtraction {
319
- /** True when the module declares a `filters` key (even an empty one). */
320
- declared: boolean;
321
- /** Top-level entries the script declares (parseable or not). */
322
- declaredCount: number;
323
- /** The renderable (declarative) subset, normalized. */
324
- filters: QueryReportFilterMeta[];
325
- }
326
-
327
- const EMPTY_EXTRACTION: ScriptFilterExtraction = { declared: false, declaredCount: 0, filters: [] };
328
-
329
- /** Filter fieldtypes the surface can render (desk-only ones are gaps). */
330
- const RENDERABLE_FILTER_FIELDTYPES = new Set([
331
- "Data",
332
- "Small Text",
333
- "Text",
334
- "Int",
335
- "Float",
336
- "Currency",
337
- "Percent",
338
- "Date",
339
- "Datetime",
340
- "Time",
341
- "Check",
342
- "Select",
343
- "Link",
344
- ]);
345
-
346
- const NUMERIC_FILTER_FIELDTYPES = new Set(["Int", "Float", "Currency", "Percent"]);
347
-
348
- /** Index AFTER the string literal opening at `start` (escape-aware). */
349
- function scanStringEnd(text: string, start: number): number {
350
- const quote = text[start];
351
- let index = start + 1;
352
- while (index < text.length) {
353
- const ch = text[index];
354
- if (ch === "\\") {
355
- index += 2;
356
- continue;
357
- }
358
- if (ch === quote) return index + 1;
359
- index += 1;
360
- }
361
- return index;
362
- }
363
-
364
- /** Index after a line or block comment opening at `start` (start+1 for a bare slash). */
365
- function scanCommentEnd(text: string, start: number): number {
366
- const next = text[start + 1];
367
- if (next === "/") {
368
- const newline = text.indexOf("\n", start);
369
- return newline === -1 ? text.length : newline + 1;
370
- }
371
- if (next === "*") {
372
- const end = text.indexOf("*/", start + 2);
373
- return end === -1 ? text.length : end + 2;
374
- }
375
- return start + 1;
376
- }
377
-
378
- /** Matching close-bracket index for the opener at `open` (-1 when unbalanced). */
379
- function matchBracket(text: string, open: number): number {
380
- const openCh = text[open];
381
- const closeCh = openCh === "[" ? "]" : openCh === "{" ? "}" : openCh === "(" ? ")" : "";
382
- if (!closeCh) return -1;
383
- let depth = 0;
384
- let index = open;
385
- while (index < text.length) {
386
- const ch = text[index];
387
- if (ch === "'" || ch === '"' || ch === "`") {
388
- index = scanStringEnd(text, index);
389
- continue;
390
- }
391
- if (ch === "/") {
392
- index = scanCommentEnd(text, index);
393
- continue;
394
- }
395
- if (ch === openCh) depth += 1;
396
- else if (ch === closeCh) {
397
- depth -= 1;
398
- if (depth === 0) return index;
399
- }
400
- index += 1;
401
- }
402
- return -1;
403
- }
404
-
405
- /** Unescape the common sequences inside a scanned string literal body. */
406
- function unescapeStringBody(body: string): string {
407
- return body.replace(/\\(.)/g, (_, ch: string) =>
408
- ch === "n" ? "\n" : ch === "t" ? "\t" : ch === "r" ? "\r" : ch,
409
- );
410
- }
411
-
412
- /**
413
- * Interpret one raw value slice from a script object literal. Returns
414
- * undefined for anything non-declarative (functions, frappe.* expressions,
415
- * template strings with interpolations, ternaries…).
416
- */
417
- function interpretScriptValue(raw: string): string | number | boolean | string[] | undefined {
418
- const value = raw.trim();
419
- if (!value) return undefined;
420
- const first = value[0];
421
- if (first === "'" || first === '"' || first === "`") {
422
- const end = scanStringEnd(value, 0);
423
- if (end !== value.length) return undefined;
424
- const body = value.slice(1, -1);
425
- if (first === "`" && body.includes("${")) return undefined;
426
- return unescapeStringBody(body);
427
- }
428
- if (/^__\s*\(/.test(value)) {
429
- // Translation call — the first string argument is the label.
430
- const parenIndex = value.indexOf("(");
431
- const rest = value.slice(parenIndex + 1).trim();
432
- const quote = rest[0];
433
- if (quote !== "'" && quote !== '"' && quote !== "`") return undefined;
434
- const quoteIndex = value.indexOf(quote, parenIndex);
435
- const end = scanStringEnd(value, quoteIndex);
436
- return unescapeStringBody(value.slice(quoteIndex + 1, end - 1));
437
- }
438
- if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
439
- if (value === "true") return true;
440
- if (value === "false") return false;
441
- if (first === "[") {
442
- const close = matchBracket(value, 0);
443
- if (close !== value.length - 1) return undefined;
444
- const entries: string[] = [];
445
- let index = 1;
446
- while (index < close) {
447
- const ch = value[index];
448
- if (ch === "'" || ch === '"' || ch === "`") {
449
- const end = scanStringEnd(value, index);
450
- const body = value.slice(index + 1, end - 1);
451
- if (ch === "`" && body.includes("${")) return undefined;
452
- entries.push(unescapeStringBody(body));
453
- index = end;
454
- continue;
455
- }
456
- if (ch === "/" && (value[index + 1] === "/" || value[index + 1] === "*")) {
457
- index = scanCommentEnd(value, index);
458
- continue;
459
- }
460
- if (ch === "," || ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
461
- index += 1;
462
- continue;
463
- }
464
- return undefined; // non-string entry (objects, expressions) — not declarative
465
- }
466
- return entries;
467
- }
468
- return undefined;
469
- }
470
-
471
- /** Parse one `{…}` object literal into declarative key/value pairs. */
472
- function parseScriptObject(source: string): Record<string, unknown> {
473
- const record: Record<string, unknown> = {};
474
- let index = 1; // past the opening brace
475
- const end = source.length - 1; // before the closing brace
476
- while (index < end) {
477
- const ch = source[index];
478
- if (ch === "/" && (source[index + 1] === "/" || source[index + 1] === "*")) {
479
- index = scanCommentEnd(source, index);
480
- continue;
481
- }
482
- if (/\s|,/.test(ch)) {
483
- index += 1;
484
- continue;
485
- }
486
- // Key: identifier or quoted string.
487
- let key = "";
488
- if (ch === "'" || ch === '"') {
489
- const keyEnd = scanStringEnd(source, index);
490
- key = source.slice(index + 1, keyEnd - 1);
491
- index = keyEnd;
492
- } else {
493
- const keyMatch = source.slice(index, index + 64).match(/^[A-Za-z_$][\w$]*/);
494
- if (!keyMatch) break; // not an object key — bail out of this entry
495
- key = keyMatch[0];
496
- index += key.length;
497
- }
498
- while (index < end && /\s/.test(source[index])) index += 1;
499
- if (source[index] !== ":") break; // shorthand/method — not declarative
500
- index += 1;
501
- // Value: scan to the next top-level comma, collecting comment-free text.
502
- let valueRaw = "";
503
- while (index < end) {
504
- const valueCh = source[index];
505
- if (valueCh === "'" || valueCh === '"' || valueCh === "`") {
506
- const stringEnd = scanStringEnd(source, index);
507
- valueRaw += source.slice(index, stringEnd);
508
- index = stringEnd;
509
- continue;
510
- }
511
- if (valueCh === "/" && (source[index + 1] === "/" || source[index + 1] === "*")) {
512
- index = scanCommentEnd(source, index);
513
- continue;
514
- }
515
- if (valueCh === "{" || valueCh === "[" || valueCh === "(") {
516
- const close = matchBracket(source, index);
517
- if (close === -1) return record; // unbalanced — stop parsing
518
- valueRaw += source.slice(index, close + 1);
519
- index = close + 1;
520
- continue;
521
- }
522
- if (valueCh === ",") {
523
- index += 1;
524
- break;
525
- }
526
- valueRaw += valueCh;
527
- index += 1;
528
- }
529
- const interpreted = interpretScriptValue(valueRaw);
530
- if (interpreted !== undefined) record[key] = interpreted;
531
- }
532
- return record;
533
- }
534
-
535
- /**
536
- * Extract the declarative filter subset from a desk report script module.
537
- * Never throws; anything unparseable degrades to a smaller `filters` list
538
- * (the `declaredCount` delta is the honest gap).
539
- */
540
- export function extractScriptFilters(script: string | undefined): ScriptFilterExtraction {
541
- if (!script) return EMPTY_EXTRACTION;
542
- const keyMatch = script.match(/\bfilters\s*:\s*\[/);
543
- if (!keyMatch || keyMatch.index === undefined) return EMPTY_EXTRACTION;
544
- const openBracket = keyMatch.index + keyMatch[0].length - 1;
545
- const closeBracket = matchBracket(script, openBracket);
546
- if (closeBracket === -1) return EMPTY_EXTRACTION;
547
- const filters: QueryReportFilterMeta[] = [];
548
- let declaredCount = 0;
549
- let index = openBracket + 1;
550
- while (index < closeBracket) {
551
- const ch = script[index];
552
- if (ch === "{") {
553
- const close = matchBracket(script, index);
554
- if (close === -1 || close > closeBracket) break;
555
- declaredCount += 1;
556
- const record = parseScriptObject(script.slice(index, close + 1));
557
- const meta = coerceFilterMetaEntry(record);
558
- const fieldtype = meta?.fieldtype ?? "Data";
559
- const needsOptions = fieldtype === "Select" || fieldtype === "Link";
560
- if (
561
- meta &&
562
- RENDERABLE_FILTER_FIELDTYPES.has(fieldtype) &&
563
- (!needsOptions || (meta.options ?? "") !== "")
564
- )
565
- filters.push({ ...meta, fieldtype });
566
- index = close + 1;
567
- continue;
568
- }
569
- if (ch === "'" || ch === '"' || ch === "`") {
570
- index = scanStringEnd(script, index);
571
- continue;
572
- }
573
- if (ch === "/" && (script[index + 1] === "/" || script[index + 1] === "*")) {
574
- index = scanCommentEnd(script, index);
575
- continue;
576
- }
577
- index += 1;
578
- }
579
- return { declared: true, declaredCount, filters };
580
- }
581
-
582
- /** `_server_messages` is a JSON array of JSON-encoded {message} objects. */
583
- function decodeServerMessages(raw: unknown): string | undefined {
584
- if (typeof raw !== "string" || !raw.startsWith("[")) return undefined;
585
- try {
586
- const entries: unknown = JSON.parse(raw);
587
- if (!Array.isArray(entries)) return undefined;
588
- const parts = entries
589
- .map((entry) => {
590
- try {
591
- const decoded: unknown = typeof entry === "string" ? JSON.parse(entry) : entry;
592
- const message = (decoded as { message?: unknown } | null)?.message;
593
- return typeof message === "string" ? stripHtml(message) : String(entry);
594
- } catch {
595
- return String(entry);
596
- }
597
- })
598
- .filter(Boolean);
599
- return parts.length > 0 ? parts.join("\n") : undefined;
600
- } catch {
601
- return undefined;
602
- }
603
- }
604
-
605
- export function humanizeFrappeError(err: unknown): Error {
606
- if (err instanceof Error) {
607
- const data = (err as { data?: unknown }).data as Record<string, unknown> | undefined;
608
- const decoded =
609
- decodeServerMessages(data?._server_messages) ?? decodeServerMessages(err.message);
610
- return decoded ? new Error(decoded) : err;
611
- }
612
- return new Error(String(err));
613
- }
1
+ // The query-report model lives in @multiplatform.one/frappe-ui (MPO-15,
2
+ // GL-28); this path keeps handing out the same names.
3
+ export {
4
+ buildRunFilters,
5
+ compactQueryReportFilters,
6
+ defaultFilterValues,
7
+ extractScriptFilters,
8
+ humanizeFrappeError,
9
+ isEmptyFilterValue,
10
+ normalizeFilterMeta,
11
+ normalizeQueryReportColumns,
12
+ normalizeQueryReportRows,
13
+ scrubFieldname,
14
+ stripReportHtml as stripHtml,
15
+ } from "@multiplatform.one/frappe-ui";
16
+ export type {
17
+ QueryReportColumn,
18
+ QueryReportFilterMeta,
19
+ QueryReportFilters,
20
+ QueryReportFixture,
21
+ QueryReportMeta,
22
+ QueryReportRow,
23
+ QueryReportRunMessage,
24
+ ScriptFilterExtraction,
25
+ } from "@multiplatform.one/frappe-ui";