@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,529 +1,4 @@
1
- // Desk-parity report spreadsheet: row numbers, sortable column headers, a
2
- // per-column FILTER ROW under the headers (desk's killer report feature —
3
- // inputs drive the standard frappe filter pipeline via reportViewModel's
4
- // kind→operator mapping), group header rows, and a totals row.
5
- //
6
- // Composed from @multiplatform.one/table's Table primitives (exported for
7
- // standalone use) rather than DataTable: DataTable owns a single internal
8
- // scrollport and wraps header content in Text, so it cannot host an input
9
- // row under its headers today — capability gap flagged to the table
10
- // workstream; this grid is the interim desk-parity surface.
11
- import { ScrollView, Text, Tooltip, View, XStack, isWeb } from "@multiplatform.one/components";
12
- import { Input } from "@multiplatform.one/forms";
13
- import { Table } from "@multiplatform.one/table";
14
- import {
15
- ensureFocusVisibleRing,
16
- formatCurrency,
17
- formatNumber,
18
- formatPercent,
19
- } from "@multiplatform.one/theme";
20
- import { CaretDownIcon, CaretUpDownIcon } from "@phosphor-icons/react";
21
- import React, { useCallback, useState } from "react";
22
- import { useTranslation } from "react-i18next";
23
- import { BACKOFFICE_I18N_NAMESPACE } from "../i18n";
24
- import { formatDateCell } from "@multiplatform.one/frappe-ui";
25
- import {
26
- REPORT_NUMERIC_FIELDTYPES,
27
- type ReportColumnDef,
28
- type ReportGridRow,
29
- type ReportRow,
30
- type ReportSortState,
31
- type ReportTotalsCell,
32
- } from "./reportViewModel";
33
-
34
- const ROW_NUMBER_WIDTH = 56;
35
-
36
- // ── LC-49/LC-50 row keyboard contract ───────────────────────────────
37
- // Local mirror of the table package's `isInteractiveInnerTarget`
38
- // (public/table/src/utils/rowPress.ts — internal util, not on the
39
- // barrel): when the whole row is one pressable unit, presses and keys
40
- // that originate on inner interactive content (Link cells, future
41
- // editors) must keep working without also firing the row press.
42
- const INTERACTIVE_SELECTOR = [
43
- "a[href]",
44
- "button",
45
- "input",
46
- "select",
47
- "textarea",
48
- "label",
49
- '[contenteditable="true"]',
50
- '[role="button"]',
51
- '[role="checkbox"]',
52
- '[role="combobox"]',
53
- '[role="link"]',
54
- '[role="menuitem"]',
55
- '[role="option"]',
56
- '[role="radio"]',
57
- '[role="slider"]',
58
- '[role="spinbutton"]',
59
- '[role="switch"]',
60
- '[role="textbox"]',
61
- "[aria-expanded]",
62
- ].join(", ");
63
-
64
- function isInteractiveInnerTarget(target: unknown, boundary: unknown): boolean {
65
- if (typeof Element === "undefined") return false;
66
- if (!(target instanceof Element)) return false;
67
- const boundaryEl = boundary instanceof Element ? boundary : null;
68
- let node: Element | null = target;
69
- while (node && node !== boundaryEl) {
70
- if (node.matches(INTERACTIVE_SELECTOR)) return true;
71
- const tabIndex = node.getAttribute("tabindex");
72
- // A tab stop of its own (0 or positive) marks a focusable control; the
73
- // row's roving -1/0 tabindex lives on the boundary, not inside it.
74
- if (tabIndex != null && Number(tabIndex) >= 0) return true;
75
- node = node.parentElement;
76
- }
77
- return false;
78
- }
79
-
80
- export interface ReportGridProps {
81
- columns: readonly ReportColumnDef[];
82
- rows: readonly ReportGridRow[];
83
- sort: ReportSortState;
84
- onSortChange: (sort: ReportSortState) => void;
85
- /** Filter-row DRAFT values by fieldname (uncommitted text). */
86
- columnFilters: Readonly<Record<string, string>>;
87
- onColumnFilterChange: (fieldname: string, value: string) => void;
88
- /** Open the row's document form. */
89
- onOpenRow: (name: string) => void;
90
- /** Open a Link cell's target document. */
91
- onOpenLink?: (doctype: string, name: string) => void;
92
- /** Totals over the full filtered result — rendered as a bottom row. */
93
- totals: readonly ReportTotalsCell[];
94
- /** Group-by column (group header rows span the grid). */
95
- groupByColumn?: ReportColumnDef;
96
- }
97
-
98
- function alignToJustify(align: ReportColumnDef["align"]) {
99
- return align === "right" ? "flex-end" : align === "center" ? "center" : "flex-start";
100
- }
101
-
102
- function formatNumericCell(value: number, fieldtype: string): string {
103
- switch (fieldtype) {
104
- case "Currency":
105
- return formatCurrency(value);
106
- case "Int":
107
- return formatNumber(Math.round(value), { maximumFractionDigits: 0 });
108
- case "Percent":
109
- return formatPercent(value, { precision: 1 });
110
- default:
111
- return formatNumber(value);
112
- }
113
- }
114
-
115
- /** Fieldtype-aware cell content (query-report cell channel, D-04 numbers). */
116
- function ReportCellValue({
117
- column,
118
- row,
119
- onOpenLink,
120
- }: {
121
- column: ReportColumnDef;
122
- row: ReportRow;
123
- onOpenLink?: (doctype: string, name: string) => void;
124
- }) {
125
- const raw = row[column.fieldname];
126
- if (raw === null || raw === undefined || raw === "") {
127
- return (
128
- <Text fontSize="$3" color="$color8">
129
- —
130
- </Text>
131
- );
132
- }
133
- if (column.fieldtype === "Check") {
134
- return (
135
- <Text fontSize="$3" color={raw ? "$green11" : "$color8"}>
136
- {raw ? "✓" : "—"}
137
- </Text>
138
- );
139
- }
140
- if (REPORT_NUMERIC_FIELDTYPES.has(column.fieldtype)) {
141
- const numeric = Number(raw);
142
- return (
143
- <Text fontSize="$3" color="$color12" fontFamily="$mono">
144
- {Number.isFinite(numeric) ? formatNumericCell(numeric, column.fieldtype) : String(raw)}
145
- </Text>
146
- );
147
- }
148
- const value = String(raw);
149
- switch (column.fieldtype) {
150
- case "Date":
151
- case "Datetime":
152
- return (
153
- <Text fontSize="$3" color="$color11" numberOfLines={1}>
154
- {formatDateCell(value)}
155
- </Text>
156
- );
157
- case "Link": {
158
- const target = column.options;
159
- if (target && onOpenLink) {
160
- return (
161
- <Text
162
- fontSize="$3"
163
- color="$color11"
164
- textDecorationLine="underline"
165
- cursor="pointer"
166
- numberOfLines={1}
167
- role="link"
168
- onPress={(event) => {
169
- event?.stopPropagation?.();
170
- onOpenLink(target, value);
171
- }}
172
- >
173
- {value}
174
- </Text>
175
- );
176
- }
177
- return (
178
- <Text fontSize="$3" color="$color11" numberOfLines={1}>
179
- {value}
180
- </Text>
181
- );
182
- }
183
- default:
184
- return (
185
- <Text fontSize="$3" color="$color11" numberOfLines={1}>
186
- {value}
187
- </Text>
188
- );
189
- }
190
- }
191
-
192
- export function ReportGrid({
193
- columns,
194
- rows,
195
- sort,
196
- onSortChange,
197
- columnFilters,
198
- onColumnFilterChange,
199
- onOpenRow,
200
- onOpenLink,
201
- totals,
202
- groupByColumn,
203
- }: ReportGridProps) {
204
- const { t } = useTranslation(BACKOFFICE_I18N_NAMESPACE);
205
-
206
- const handleSortPress = useCallback(
207
- (fieldname: string) => {
208
- if (sort.field === fieldname) {
209
- onSortChange({ field: fieldname, order: sort.order === "asc" ? "desc" : "asc" });
210
- } else {
211
- onSortChange({ field: fieldname, order: "asc" });
212
- }
213
- },
214
- [sort, onSortChange],
215
- );
216
-
217
- // ── Row press + keyboard (LC-49 ONE-TAB-STOP, LC-50 KEY-ACTIVATION) ──
218
- // Mirrors DataTable's getRowPressProps contract (DataTableRoot): the
219
- // DATA-row set exposes exactly one tab stop (roving anchor — the focused
220
- // row while focus is inside the set, else the first row); Enter AND
221
- // Space each fire exactly one activation; arrows/Home/End rove focus;
222
- // inner interactive content (Link cells) wins over the row press.
223
- const isSSR = typeof window === "undefined";
224
- const [rowAnchor, setRowAnchor] = useState(0);
225
- const dataRowCount = rows.reduce((count, row) => (row.kind === "row" ? count + 1 : count), 0);
226
- const effectiveRowAnchor = Math.min(rowAnchor, Math.max(dataRowCount - 1, 0));
227
-
228
- const getRowPressProps = useCallback(
229
- (row: ReportRow, pressIndex: number) => {
230
- const handlePress = (
231
- event:
232
- | { defaultPrevented?: boolean; target?: unknown; nativeEvent?: { target?: unknown } }
233
- | undefined,
234
- ) => {
235
- if (event?.defaultPrevented) return;
236
- const targetEl = event?.target ?? event?.nativeEvent?.target;
237
- if (isWeb && typeof Element !== "undefined" && targetEl instanceof Element) {
238
- const boundary = targetEl.closest('[data-report-row="true"]');
239
- if (isInteractiveInnerTarget(targetEl, boundary)) return;
240
- }
241
- onOpenRow(row.name);
242
- };
243
- return {
244
- // Accessible row name from doc identity (DG-A11Y-06); positional
245
- // fallback for the defensive empty-name case.
246
- "aria-label":
247
- row.name || t("report.rowLabel", { defaultValue: "Row {{row}}", row: pressIndex + 1 }),
248
- onPress: handlePress,
249
- cursor: "pointer" as const,
250
- hoverStyle: { backgroundColor: "$color2" },
251
- pressStyle: { backgroundColor: "$color4" },
252
- ...(isWeb && !isSSR
253
- ? {
254
- tabIndex: pressIndex === effectiveRowAnchor ? 0 : -1,
255
- outlineWidth: 0,
256
- focusStyle: { outlineWidth: 0 },
257
- focusVisibleStyle: ensureFocusVisibleRing({ outlineOffset: -2 }),
258
- "data-report-row": "true",
259
- onFocus: (event: React.FocusEvent) => {
260
- // Only a focused ROW moves the anchor; focus on an inner
261
- // control (own tab stop) leaves the roving state alone.
262
- if (event.target === event.currentTarget) setRowAnchor(pressIndex);
263
- },
264
- onBlur: (event: React.FocusEvent) => {
265
- const next = event.relatedTarget;
266
- if (!(next instanceof Element) || !next.closest('[data-report-row="true"]')) {
267
- setRowAnchor(0);
268
- }
269
- },
270
- onKeyDown: (event: React.KeyboardEvent) => {
271
- if (event.defaultPrevented) return;
272
- // Keys on focused inner controls belong to those controls.
273
- if (event.target !== event.currentTarget) return;
274
- const key = event.key;
275
- if (key === "Enter" || key === " " || key === "Spacebar") {
276
- // preventDefault stops Space page-scroll; one physical
277
- // key press = one activation (auto-repeat guarded).
278
- event.preventDefault();
279
- event.stopPropagation();
280
- if (event.repeat) return;
281
- onOpenRow(row.name);
282
- return;
283
- }
284
- if (key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
285
- event.preventDefault();
286
- event.stopPropagation();
287
- const rowEl = event.currentTarget as HTMLElement;
288
- const scope = rowEl.closest('[data-report-grid="true"]') ?? rowEl.ownerDocument;
289
- const pressRows = Array.from(
290
- scope.querySelectorAll('[data-report-row="true"]'),
291
- ) as HTMLElement[];
292
- const currentIdx = pressRows.indexOf(rowEl);
293
- if (currentIdx < 0) return;
294
- const nextIdx =
295
- key === "Home"
296
- ? 0
297
- : key === "End"
298
- ? pressRows.length - 1
299
- : Math.min(
300
- Math.max(currentIdx + (key === "ArrowDown" ? 1 : -1), 0),
301
- pressRows.length - 1,
302
- );
303
- pressRows[nextIdx]?.focus();
304
- }
305
- },
306
- }
307
- : {}),
308
- };
309
- },
310
- [onOpenRow, effectiveRowAnchor, isSSR, t],
311
- );
312
-
313
- const totalsByField = new Map(totals.map((total) => [total.fieldname, total]));
314
- const gridWidth = ROW_NUMBER_WIDTH + columns.reduce((sum, column) => sum + column.width, 0);
315
-
316
- return (
317
- <ScrollView horizontal showsHorizontalScrollIndicator flexGrow={0} flexShrink={1}>
318
- <Table
319
- minWidth={gridWidth}
320
- width={gridWidth}
321
- displayMode="flex"
322
- cellHeight="$3"
323
- // Roving scope marker: arrow-key navigation queries pressable rows
324
- // within this container (getRowPressProps).
325
- {...({ "data-report-grid": "true" } as Record<string, unknown>)}
326
- >
327
- <Table.Head>
328
- {/* Header row: label + sort affordance (aria-sort, Enter/Space). */}
329
- <Table.Row>
330
- <Table.HeaderCell
331
- cellWidth={ROW_NUMBER_WIDTH}
332
- aria-label={t("report.rowNumber", "Row number")}
333
- >
334
- <Text fontSize="$2" color="$color9">
335
- #
336
- </Text>
337
- </Table.HeaderCell>
338
- {columns.map((column) => {
339
- const sorted = sort.field === column.fieldname ? sort.order : undefined;
340
- return (
341
- <Table.HeaderCell
342
- key={column.fieldname}
343
- cellWidth={column.width}
344
- justifyContent={alignToJustify(column.align)}
345
- aria-sort={
346
- sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none"
347
- }
348
- >
349
- <Tooltip
350
- content={
351
- sorted === "asc"
352
- ? t("report.sortDesc", "Sorted ascending (press to sort descending)")
353
- : sorted === "desc"
354
- ? t("report.sortAsc", "Sorted descending (press to sort ascending)")
355
- : t("report.sortColumn", "Sort by this column")
356
- }
357
- >
358
- <XStack
359
- gap="$1.5"
360
- alignItems="center"
361
- cursor="pointer"
362
- role="button"
363
- tabIndex={0}
364
- outlineWidth={0}
365
- focusStyle={{ outlineWidth: 0 }}
366
- focusVisibleStyle={ensureFocusVisibleRing({ outlineOffset: -2 })}
367
- aria-label={t("report.sortBy", {
368
- defaultValue: "Sort by {{column}}",
369
- column: column.label,
370
- })}
371
- onPress={() => handleSortPress(column.fieldname)}
372
- onKeyDown={(event: { key?: string; preventDefault?: () => void }) => {
373
- if (event.key === "Enter" || event.key === " ") {
374
- event.preventDefault?.();
375
- handleSortPress(column.fieldname);
376
- }
377
- }}
378
- hoverStyle={{ opacity: 0.8 }}
379
- >
380
- <Text fontSize="$3" fontWeight="400" numberOfLines={1}>
381
- {column.label}
382
- </Text>
383
- {/* Caret orientation mirrors DataTable's headers:
384
- asc = caret-down, desc = rotated 180. */}
385
- <View aria-hidden role="presentation">
386
- {sorted ? (
387
- <CaretDownIcon
388
- size={12}
389
- color="var(--color10)"
390
- style={sorted === "desc" ? { transform: "rotate(180deg)" } : undefined}
391
- />
392
- ) : (
393
- <CaretUpDownIcon size={12} color="var(--color8)" />
394
- )}
395
- </View>
396
- </XStack>
397
- </Tooltip>
398
- </Table.HeaderCell>
399
- );
400
- })}
401
- </Table.Row>
402
- {/* Filter row — desk parity: one input per column, standard filter
403
- pipeline. Number/date accept comparison prefixes (>, <, >=…). */}
404
- <Table.Row>
405
- <Table.HeaderCell cellWidth={ROW_NUMBER_WIDTH} paddingVertical="$1.5" />
406
- {columns.map((column) => {
407
- const placeholder =
408
- column.kind === "number" || column.kind === "date"
409
- ? t("report.filterComparablePlaceholder", "= > <…")
410
- : column.kind === "check"
411
- ? t("report.filterCheckPlaceholder", "yes / no")
412
- : t("report.filterPlaceholder", "Filter…");
413
- return (
414
- <Table.HeaderCell
415
- key={column.fieldname}
416
- cellWidth={column.width}
417
- paddingVertical="$1.5"
418
- overflow="visible"
419
- >
420
- <View flex={1} minWidth={0} overflow="visible">
421
- <Input
422
- size="$2"
423
- compact
424
- value={columnFilters[column.fieldname] ?? ""}
425
- onChangeText={(next) => onColumnFilterChange(column.fieldname, next)}
426
- placeholder={placeholder}
427
- // inputProps is the eject that reaches the real input
428
- // element — a top-level aria-label stays on the wrapper.
429
- inputProps={{
430
- "aria-label": t("report.filterColumn", {
431
- defaultValue: "Filter {{column}}",
432
- column: column.label,
433
- }),
434
- }}
435
- />
436
- </View>
437
- </Table.HeaderCell>
438
- );
439
- })}
440
- </Table.Row>
441
- </Table.Head>
442
- <Table.Body>
443
- {(() => {
444
- // Position within the pressable DATA-row set (group headers are
445
- // not pressable and hold no tab stop).
446
- let pressCursor = -1;
447
- return rows.map((gridRow) => {
448
- if (gridRow.kind === "group") {
449
- const label =
450
- gridRow.value === null || gridRow.value === undefined || gridRow.value === ""
451
- ? t("report.groupEmpty", "(blank)")
452
- : String(gridRow.value);
453
- return (
454
- <Table.Row key={gridRow.key} backgroundColor="$color2">
455
- <Table.Cell cellWidth={gridWidth} paddingVertical="$1.5" role="rowheader">
456
- <Text fontSize="$2" fontWeight="400" color="$color11" numberOfLines={1}>
457
- {groupByColumn
458
- ? t("report.groupHeader", {
459
- defaultValue: "{{field}}: {{value}} · {{count}}",
460
- field: groupByColumn.label,
461
- value: label,
462
- count: gridRow.count,
463
- })
464
- : label}
465
- </Text>
466
- </Table.Cell>
467
- </Table.Row>
468
- );
469
- }
470
- pressCursor += 1;
471
- return (
472
- // Spread LAST so the roving tabIndex/handlers stay in
473
- // charge of this row's interactivity (DataTable precedent).
474
- <Table.Row
475
- key={gridRow.key}
476
- {...(getRowPressProps(gridRow.row, pressCursor) as Record<string, unknown>)}
477
- >
478
- <Table.Cell cellWidth={ROW_NUMBER_WIDTH} paddingVertical="$2">
479
- <Text fontSize="$2" color="$color9" fontFamily="$mono">
480
- {gridRow.index}
481
- </Text>
482
- </Table.Cell>
483
- {columns.map((column) => (
484
- <Table.Cell
485
- key={column.fieldname}
486
- cellWidth={column.width}
487
- paddingVertical="$2"
488
- justifyContent={alignToJustify(column.align)}
489
- >
490
- <ReportCellValue column={column} row={gridRow.row} onOpenLink={onOpenLink} />
491
- </Table.Cell>
492
- ))}
493
- </Table.Row>
494
- );
495
- });
496
- })()}
497
- </Table.Body>
498
- {totals.length > 0 && (
499
- <Table.Foot borderTopWidth={1} borderTopColor="$color6">
500
- <Table.Row backgroundColor="$color2">
501
- <Table.Cell cellWidth={ROW_NUMBER_WIDTH} paddingVertical="$2">
502
- <Text fontSize="$2" fontWeight="400" color="$color10">
503
- {t("report.totals", "Totals")}
504
- </Text>
505
- </Table.Cell>
506
- {columns.map((column) => {
507
- const total = totalsByField.get(column.fieldname);
508
- return (
509
- <Table.Cell
510
- key={column.fieldname}
511
- cellWidth={column.width}
512
- paddingVertical="$2"
513
- justifyContent={alignToJustify(column.align)}
514
- >
515
- {total ? (
516
- <Text fontSize="$3" fontWeight="400" color="$color12" fontFamily="$mono">
517
- {formatNumericCell(total.value, total.fieldtype)}
518
- </Text>
519
- ) : null}
520
- </Table.Cell>
521
- );
522
- })}
523
- </Table.Row>
524
- </Table.Foot>
525
- )}
526
- </Table>
527
- </ScrollView>
528
- );
529
- }
1
+ // The report grid lives in @multiplatform.one/frappe-ui (MPO-15, GL-28);
2
+ // this path keeps handing out the same names.
3
+ export { ReportGrid } from "@multiplatform.one/frappe-ui";
4
+ export type { ReportGridProps } from "@multiplatform.one/frappe-ui";