@multiplatform.one/backoffice 7.28.1 → 7.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,175 +0,0 @@
1
- /**
2
- * ReportGrid row keyboard contract (LC-49 ONE-TAB-STOP, LC-50
3
- * KEY-ACTIVATION, DG-A11Y-06 names).
4
- *
5
- * The data row is ONE pressable unit: click anywhere activates, Enter AND
6
- * Space activate from the keyboard, the data-row set exposes exactly one
7
- * tab stop (roving tabindex, arrows/Home/End move it), rows are named by
8
- * doc identity, presses on inner interactive content (Link cells) never
9
- * reach the row handler, and there is no per-cell pressable chrome inside
10
- * body cells. Group header rows are not pressable and hold no tab stop.
11
- */
12
- import { renderWithProviders } from "@multiplatform.one/test-utils";
13
- import { fireEvent, screen, waitFor } from "@testing-library/react";
14
- import React from "react";
15
- import { describe, expect, it, vi } from "vitest";
16
- import { ReportGrid, type ReportGridProps } from "./report-grid";
17
- import { buildGridRows, type ReportColumnDef, type ReportRow } from "./reportViewModel";
18
-
19
- const columns: ReportColumnDef[] = [
20
- { fieldname: "name", label: "ID", fieldtype: "Data", kind: "text", width: 160, align: "left" },
21
- {
22
- fieldname: "status",
23
- label: "Status",
24
- fieldtype: "Select",
25
- kind: "select",
26
- options: "Open\nClosed",
27
- width: 120,
28
- align: "left",
29
- },
30
- {
31
- fieldname: "assigned_by",
32
- label: "Assigned By",
33
- fieldtype: "Link",
34
- kind: "link",
35
- options: "User",
36
- width: 160,
37
- align: "left",
38
- },
39
- ];
40
-
41
- const dataRows: ReportRow[] = [
42
- { name: "TD-0001", status: "Open", assigned_by: "alice@example.com" },
43
- { name: "TD-0002", status: "Closed", assigned_by: "bob@example.com" },
44
- { name: "TD-0003", status: "Open", assigned_by: "carol@example.com" },
45
- ];
46
-
47
- function pressRows(container: HTMLElement): HTMLElement[] {
48
- return Array.from(container.querySelectorAll('[data-report-row="true"]')) as HTMLElement[];
49
- }
50
-
51
- function renderGrid(overrides: Partial<ReportGridProps> = {}) {
52
- const onOpenRow = vi.fn();
53
- const onOpenLink = vi.fn();
54
- const result = renderWithProviders(
55
- <ReportGrid
56
- columns={columns}
57
- rows={buildGridRows(dataRows, 0, 20)}
58
- sort={{ field: "modified", order: "desc" }}
59
- onSortChange={vi.fn()}
60
- columnFilters={{}}
61
- onColumnFilterChange={vi.fn()}
62
- onOpenRow={onOpenRow}
63
- onOpenLink={onOpenLink}
64
- totals={[]}
65
- {...overrides}
66
- />,
67
- );
68
- return { ...result, onOpenRow, onOpenLink };
69
- }
70
-
71
- describe("ReportGrid row activation (LC-50)", () => {
72
- it("activates on a click anywhere in the row with the row's name", () => {
73
- const { onOpenRow } = renderGrid();
74
- fireEvent.click(screen.getByText("Closed"));
75
- expect(onOpenRow).toHaveBeenCalledTimes(1);
76
- expect(onOpenRow).toHaveBeenCalledWith("TD-0002");
77
- });
78
-
79
- it("activates on Enter AND Space, once per physical key press", () => {
80
- const { container, onOpenRow } = renderGrid();
81
- const rows = pressRows(container);
82
- rows[0].focus();
83
-
84
- fireEvent.keyDown(rows[0], { key: "Enter" });
85
- expect(onOpenRow).toHaveBeenCalledTimes(1);
86
- expect(onOpenRow).toHaveBeenLastCalledWith("TD-0001");
87
-
88
- fireEvent.keyDown(rows[0], { key: " " });
89
- expect(onOpenRow).toHaveBeenCalledTimes(2);
90
-
91
- // Key auto-repeat never machine-guns activations.
92
- fireEvent.keyDown(rows[0], { key: "Enter", repeat: true });
93
- expect(onOpenRow).toHaveBeenCalledTimes(2);
94
- });
95
-
96
- it("names each pressable row by doc identity (DG-A11Y-06)", () => {
97
- const { container } = renderGrid();
98
- expect(pressRows(container).map((row) => row.getAttribute("aria-label"))).toEqual([
99
- "TD-0001",
100
- "TD-0002",
101
- "TD-0003",
102
- ]);
103
- });
104
- });
105
-
106
- describe("ReportGrid roving tab stop (LC-49)", () => {
107
- it("exposes exactly one tab stop; arrows and Home/End rove it", async () => {
108
- const { container } = renderGrid();
109
- const rows = pressRows(container);
110
- expect(rows).toHaveLength(3);
111
- expect(rows.map((row) => row.getAttribute("tabindex"))).toEqual(["0", "-1", "-1"]);
112
-
113
- rows[0].focus();
114
- expect(document.activeElement).toBe(rows[0]);
115
-
116
- fireEvent.keyDown(rows[0], { key: "ArrowDown" });
117
- expect(document.activeElement).toBe(rows[1]);
118
- // The anchor follows focus: still exactly one tabindex=0.
119
- await waitFor(() => {
120
- const stops = pressRows(container).map((row) => row.getAttribute("tabindex"));
121
- expect(stops).toEqual(["-1", "0", "-1"]);
122
- });
123
-
124
- fireEvent.keyDown(rows[1], { key: "End" });
125
- expect(document.activeElement).toBe(rows[2]);
126
- fireEvent.keyDown(rows[2], { key: "Home" });
127
- expect(document.activeElement).toBe(rows[0]);
128
- fireEvent.keyDown(rows[0], { key: "ArrowUp" });
129
- // Clamped at the first row — no wrap.
130
- expect(document.activeElement).toBe(rows[0]);
131
- });
132
-
133
- it("group header rows are not pressable and hold no tab stop", () => {
134
- const { container } = renderGrid({
135
- rows: buildGridRows(dataRows, 0, 20, "status"),
136
- groupByColumn: columns[1],
137
- });
138
- // Three data rows stay pressable; the injected group headers are not.
139
- expect(pressRows(container)).toHaveLength(3);
140
- const groupHeaderCells = Array.from(container.querySelectorAll('[role="rowheader"]'));
141
- expect(groupHeaderCells.length).toBeGreaterThan(0);
142
- for (const cell of groupHeaderCells) {
143
- const groupRow = cell.closest('[role="row"]') as HTMLElement;
144
- expect(groupRow.getAttribute("data-report-row")).toBeNull();
145
- expect(groupRow.getAttribute("tabindex")).toBeNull();
146
- }
147
- });
148
- });
149
-
150
- describe("ReportGrid inner interactive content wins", () => {
151
- it("a Link cell press opens the linked doc, never the row", () => {
152
- const { onOpenRow, onOpenLink } = renderGrid();
153
- fireEvent.click(screen.getByText("alice@example.com"));
154
- expect(onOpenLink).toHaveBeenCalledTimes(1);
155
- expect(onOpenLink).toHaveBeenCalledWith("User", "alice@example.com");
156
- expect(onOpenRow).not.toHaveBeenCalled();
157
- });
158
-
159
- it("keys on focused inner content belong to that content, not the row", () => {
160
- const { onOpenRow } = renderGrid();
161
- // Keydown dispatched from the link bubbles to the row, where the
162
- // target !== currentTarget guard hands it back to the inner control.
163
- fireEvent.keyDown(screen.getByText("alice@example.com"), { key: "Enter" });
164
- expect(onOpenRow).not.toHaveBeenCalled();
165
- });
166
-
167
- it("renders no per-cell pressable chrome inside body cells", () => {
168
- const { container } = renderGrid();
169
- const body = container.querySelector("tbody");
170
- expect(body).not.toBeNull();
171
- // Link cells are the only sanctioned inner interactive content; no
172
- // per-cell buttons wrap plain values.
173
- expect(body!.querySelectorAll('[role="button"], button')).toHaveLength(0);
174
- });
175
- });
@@ -1,90 +0,0 @@
1
- /**
2
- * ReportToolbar DOM prop hygiene (LC-46 class).
3
- *
4
- * The Save As popover previously passed `elevate` to raw tamagui
5
- * Popover.Content, which has no such variant in tamagui 2.0 — the bare
6
- * boolean fell through to the DOM element and fired React's
7
- * non-boolean-attribute console.error on every popover open (and painted
8
- * no shadow: the "styling" was dead). The surface now rides the house
9
- * PopoverContent (elevatedSurface knob recipe). Render + open → zero
10
- * unknown-prop / non-boolean-attribute console errors.
11
- */
12
- import { renderWithProviders } from "@multiplatform.one/test-utils";
13
- import { fireEvent, waitFor } from "@testing-library/react";
14
- import React from "react";
15
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
- import type { ReportColumnDef } from "./reportViewModel";
17
- import { ReportToolbar } from "./report-toolbar";
18
-
19
- const DOM_PROP_LEAK =
20
- /does not recognize the `.+` prop on a DOM element|non-boolean attribute|Invalid DOM property/i;
21
-
22
- const columns: ReportColumnDef[] = [
23
- { fieldname: "name", label: "ID", fieldtype: "Data", kind: "text", width: 160, align: "left" },
24
- {
25
- fieldname: "status",
26
- label: "Status",
27
- fieldtype: "Select",
28
- kind: "select",
29
- options: "Open\nClosed",
30
- width: 120,
31
- align: "left",
32
- },
33
- ];
34
-
35
- function renderToolbar() {
36
- return renderWithProviders(
37
- <ReportToolbar
38
- columns={columns}
39
- selected={["name", "status"]}
40
- onToggleColumn={vi.fn()}
41
- onResetColumns={vi.fn()}
42
- groupBy={null}
43
- onGroupByChange={vi.fn()}
44
- baseFilters={[]}
45
- columnFilterCount={0}
46
- onClearColumnFilters={vi.fn()}
47
- savedReports={[]}
48
- currentReport={null}
49
- onSelectReport={vi.fn()}
50
- onSaveReport={vi.fn()}
51
- onDeleteReport={vi.fn()}
52
- onExport={vi.fn()}
53
- />,
54
- );
55
- }
56
-
57
- let errorSpy: ReturnType<typeof vi.spyOn> | null = null;
58
- beforeEach(() => {
59
- // Strict local spy: the shared test setup filters known leak warnings
60
- // globally, but this arm must stay falsifiable — capture everything.
61
- errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
62
- });
63
- afterEach(() => {
64
- errorSpy?.mockRestore();
65
- errorSpy = null;
66
- });
67
-
68
- function leakMessages(): string[] {
69
- return (errorSpy?.mock.calls ?? [])
70
- .map((args: unknown[]) => args.map(String).join(" "))
71
- .filter((message) => DOM_PROP_LEAK.test(message));
72
- }
73
-
74
- describe("ReportToolbar Save As popover (LC-46 DOM prop hygiene)", () => {
75
- it("opens the Save As popover without leaking non-DOM props", async () => {
76
- const result = renderToolbar();
77
-
78
- const saveAs = result.findTextElement("Save As…");
79
- expect(saveAs).toBeDefined();
80
- fireEvent.click(saveAs as Element);
81
-
82
- await waitFor(() => {
83
- expect(document.querySelector('input[placeholder="New report name"]')).toBeTruthy();
84
- });
85
-
86
- expect(leakMessages()).toEqual([]);
87
- // Belt-and-braces: no leaked attribute rendered anywhere either.
88
- expect(document.querySelector("[elevate]")).toBeNull();
89
- });
90
- });
@@ -1,391 +0,0 @@
1
- // Normalizer specs pinned to PROBED bench payloads (see reportShapes.ts
2
- // header): v15 dict columns/rows from frappe.desk.query_report.run plus the
3
- // defensive old-style string-column / positional-array-row forms, and the
4
- // script-module filter extraction (probed get_script payloads).
5
- import { describe, expect, it } from "vitest";
6
- import {
7
- buildRunFilters,
8
- compactQueryReportFilters,
9
- defaultFilterValues,
10
- extractScriptFilters,
11
- normalizeFilterMeta,
12
- normalizeQueryReportColumns,
13
- normalizeQueryReportRows,
14
- scrubFieldname,
15
- stripHtml,
16
- } from "./reportShapes";
17
-
18
- describe("normalizeQueryReportColumns", () => {
19
- it("normalizes v15 dict columns (probed ToDo report shape)", () => {
20
- const columns = normalizeQueryReportColumns([
21
- { fieldtype: "Link", options: "ToDo", width: "90", label: "ID", fieldname: "id" },
22
- { fieldtype: "", width: "60", label: "Priority", fieldname: "priority" },
23
- { fieldtype: "Date", label: "Date", fieldname: "date" },
24
- {
25
- fieldtype: "Data",
26
- width: "120",
27
- label: "Assigned To/Owner",
28
- fieldname: "assigned_to/owner",
29
- },
30
- ]);
31
- expect(columns).toEqual([
32
- { fieldname: "id", label: "ID", fieldtype: "Link", options: "ToDo", width: 90 },
33
- { fieldname: "priority", label: "Priority", fieldtype: "", options: undefined, width: 60 },
34
- { fieldname: "date", label: "Date", fieldtype: "Date", options: undefined, width: undefined },
35
- {
36
- fieldname: "assigned_to/owner",
37
- label: "Assigned To/Owner",
38
- fieldtype: "Data",
39
- options: undefined,
40
- width: 120,
41
- },
42
- ]);
43
- });
44
-
45
- it("normalizes old-style SQL alias strings", () => {
46
- const columns = normalizeQueryReportColumns([
47
- "ID:Link/ToDo:120",
48
- "Status::100",
49
- "Modified On:Date:110",
50
- ]);
51
- expect(columns).toEqual([
52
- { fieldname: "id", label: "ID", fieldtype: "Link", options: "ToDo", width: 120 },
53
- { fieldname: "status", label: "Status", fieldtype: "", options: undefined, width: 100 },
54
- {
55
- fieldname: "modified_on",
56
- label: "Modified On",
57
- fieldtype: "Date",
58
- options: undefined,
59
- width: 110,
60
- },
61
- ]);
62
- });
63
-
64
- it("dedupes colliding fieldnames and skips junk entries", () => {
65
- const columns = normalizeQueryReportColumns(["Total::80", "Total::80", null, 42]);
66
- expect(columns.map((column) => column.fieldname)).toEqual(["total", "total_1"]);
67
- });
68
- });
69
-
70
- describe("normalizeQueryReportRows", () => {
71
- const columns = normalizeQueryReportColumns(["ID:Link/ToDo:120", "Status::100"]);
72
-
73
- it("passes dict rows through (probed v15 shape)", () => {
74
- const rows = normalizeQueryReportRows([{ id: "TD-1", status: "Open" }], columns);
75
- expect(rows).toEqual([{ id: "TD-1", status: "Open" }]);
76
- });
77
-
78
- it("zips positional array rows onto column fieldnames", () => {
79
- const rows = normalizeQueryReportRows([["TD-1", "Open"]], columns);
80
- expect(rows).toEqual([{ id: "TD-1", status: "Open" }]);
81
- });
82
-
83
- it("drops scalar/null separator entries", () => {
84
- expect(normalizeQueryReportRows([null, "sep", 3, ["TD-1", "Open"]], columns)).toHaveLength(1);
85
- });
86
- });
87
-
88
- describe("scrubFieldname", () => {
89
- it("matches frappe.scrub semantics", () => {
90
- expect(scrubFieldname("Modified On")).toBe("modified_on");
91
- expect(scrubFieldname("Assigned To/Owner")).toBe("assigned_to_owner");
92
- expect(scrubFieldname(" Total (USD) ")).toBe("total_usd");
93
- });
94
- });
95
-
96
- describe("stripHtml", () => {
97
- it("flattens desk anchor markup (probed ToDo reference cell)", () => {
98
- expect(stripHtml('<a href="/app/Form/ToDo/oftgr5usq3">ToDo: oftgr5usq3</a>')).toBe(
99
- "ToDo: oftgr5usq3",
100
- );
101
- });
102
-
103
- it("decodes basic entities and leaves plain text alone", () => {
104
- expect(stripHtml("A &amp; B")).toBe("A & B");
105
- expect(stripHtml("plain")).toBe("plain");
106
- });
107
- });
108
-
109
- describe("normalizeFilterMeta", () => {
110
- it("normalizes Report Filter doc rows (mandatory flag, wildcard, defaults)", () => {
111
- expect(
112
- normalizeFilterMeta([
113
- {
114
- fieldname: "status",
115
- label: "Status",
116
- fieldtype: "Select",
117
- options: "Open\nClosed",
118
- mandatory: 1,
119
- wildcard_filter: 0,
120
- default: "Open",
121
- },
122
- { label: "Search Term", fieldtype: "Data", wildcard_filter: "1" },
123
- null,
124
- "junk",
125
- ]),
126
- ).toEqual([
127
- {
128
- fieldname: "status",
129
- label: "Status",
130
- fieldtype: "Select",
131
- options: "Open\nClosed",
132
- default: "Open",
133
- mandatory: 1,
134
- wildcard_filter: 0,
135
- hidden: 0,
136
- read_only: 0,
137
- },
138
- {
139
- fieldname: "search_term",
140
- label: "Search Term",
141
- fieldtype: "Data",
142
- options: undefined,
143
- default: undefined,
144
- mandatory: 0,
145
- wildcard_filter: 1,
146
- hidden: 0,
147
- read_only: 0,
148
- },
149
- ]);
150
- });
151
-
152
- it("accepts script vocabulary: reqd, boolean/number defaults, options arrays", () => {
153
- const [filter] = normalizeFilterMeta([
154
- {
155
- fieldname: "show_permissions",
156
- fieldtype: "Check",
157
- reqd: 1,
158
- default: true,
159
- options: ["", "Open", "Closed"],
160
- },
161
- ]);
162
- expect(filter).toMatchObject({
163
- fieldname: "show_permissions",
164
- mandatory: 1,
165
- default: 1,
166
- options: "\nOpen\nClosed",
167
- });
168
- });
169
-
170
- it("honors read_only / readOnly so locked filters stay visible but inert", () => {
171
- const [locked] = normalizeFilterMeta([
172
- { fieldname: "company", fieldtype: "Link", options: "Company", read_only: 1 },
173
- ]);
174
- const [alsoLocked] = normalizeFilterMeta([
175
- { fieldname: "company", fieldtype: "Link", options: "Company", readOnly: true },
176
- ]);
177
- expect(locked).toMatchObject({ read_only: 1 });
178
- expect(alsoLocked).toMatchObject({ read_only: 1 });
179
- });
180
- });
181
-
182
- describe("extractScriptFilters", () => {
183
- it("extracts the declarative subset (probed Permitted Documents For User script)", () => {
184
- // Verbatim get_script payload from the bench probe (2026-08-13) —
185
- // includes a get_query function that must be skipped, not parsed.
186
- const script = `// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
187
- // MIT License. See license.txt
188
-
189
- frappe.query_reports["Permitted Documents For User"] = {
190
- \tfilters: [
191
- \t\t{
192
- \t\t\tfieldname: "user",
193
- \t\t\tlabel: __("User"),
194
- \t\t\tfieldtype: "Link",
195
- \t\t\toptions: "User",
196
- \t\t\treqd: 1,
197
- \t\t},
198
- \t\t{
199
- \t\t\tfieldname: "doctype",
200
- \t\t\tlabel: __("DocType"),
201
- \t\t\tfieldtype: "Link",
202
- \t\t\toptions: "DocType",
203
- \t\t\treqd: 1,
204
- \t\t\tget_query: function () {
205
- \t\t\t\treturn {
206
- \t\t\t\t\tquery: "frappe.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes",
207
- \t\t\t\t\tfilters: {
208
- \t\t\t\t\t\tuser: frappe.query_report.get_filter_value("user"),
209
- \t\t\t\t\t},
210
- \t\t\t\t};
211
- \t\t\t},
212
- \t\t},
213
- \t\t{
214
- \t\t\tfieldname: "show_permissions",
215
- \t\t\tlabel: __("Show Permissions"),
216
- \t\t\tfieldtype: "Check",
217
- \t\t},
218
- \t],
219
- };
220
-
221
- //# sourceURL=permitted_documents_for_user.js`;
222
- const extraction = extractScriptFilters(script);
223
- expect(extraction.declared).toBe(true);
224
- expect(extraction.declaredCount).toBe(3);
225
- expect(extraction.filters).toEqual([
226
- {
227
- fieldname: "user",
228
- label: "User",
229
- fieldtype: "Link",
230
- options: "User",
231
- default: undefined,
232
- mandatory: 1,
233
- wildcard_filter: 0,
234
- hidden: 0,
235
- read_only: 0,
236
- },
237
- {
238
- fieldname: "doctype",
239
- label: "DocType",
240
- fieldtype: "Link",
241
- options: "DocType",
242
- default: undefined,
243
- mandatory: 1,
244
- wildcard_filter: 0,
245
- hidden: 0,
246
- read_only: 0,
247
- },
248
- {
249
- fieldname: "show_permissions",
250
- label: "Show Permissions",
251
- fieldtype: "Check",
252
- options: undefined,
253
- default: undefined,
254
- mandatory: 0,
255
- wildcard_filter: 0,
256
- hidden: 0,
257
- read_only: 0,
258
- },
259
- ]);
260
- });
261
-
262
- it("treats an empty declared array as declared with no filters (probed ToDo script)", () => {
263
- const extraction = extractScriptFilters(
264
- 'frappe.query_reports["ToDo"] = {\n\tfilters: [],\n};\n',
265
- );
266
- expect(extraction).toEqual({ declared: true, declaredCount: 0, filters: [] });
267
- });
268
-
269
- it("reports no declaration for scripts without a filters key (and for no script)", () => {
270
- expect(extractScriptFilters('frappe.query_reports["X"] = {};').declared).toBe(false);
271
- expect(extractScriptFilters(undefined).declared).toBe(false);
272
- });
273
-
274
- it("counts non-declarative and desk-only entries as gaps", () => {
275
- const script = `frappe.query_reports["X"] = {
276
- filters: [
277
- { fieldname: "user", fieldtype: "Link", options: "User" },
278
- { fieldname: dynamic_name, fieldtype: "Data" },
279
- { fieldname: "companies", label: __("Companies"), fieldtype: "MultiSelectList",
280
- get_data: function (txt) { return frappe.db.get_link_options("Company", txt); } },
281
- ],
282
- };`;
283
- const extraction = extractScriptFilters(script);
284
- expect(extraction.declaredCount).toBe(3);
285
- expect(extraction.filters.map((filter) => filter.fieldname)).toEqual(["user"]);
286
- });
287
-
288
- it("skips dynamic defaults but keeps scalar ones (numbers, quoted strings, arrays)", () => {
289
- const script = `frappe.query_reports["X"] = {
290
- filters: [
291
- { fieldname: "from_date", fieldtype: "Date", default: frappe.datetime.get_today() },
292
- { fieldname: "status", fieldtype: "Select", options: ["", "Open", "Closed"], default: "Open" },
293
- { fieldname: "limit", fieldtype: "Int", default: 20 },
294
- // trailing comment inside the array
295
- { fieldname: "hidden_flag", fieldtype: "Check", default: 1, hidden: 1 },
296
- ],
297
- };`;
298
- const extraction = extractScriptFilters(script);
299
- expect(extraction.declaredCount).toBe(4);
300
- expect(extraction.filters).toHaveLength(4);
301
- const [fromDate, status, limit, hiddenFlag] = extraction.filters;
302
- expect(fromDate).toMatchObject({ fieldname: "from_date", default: undefined });
303
- expect(status).toMatchObject({ options: "\nOpen\nClosed", default: "Open" });
304
- expect(limit).toMatchObject({ default: 20 });
305
- expect(hiddenFlag).toMatchObject({ hidden: 1, default: 1 });
306
- });
307
-
308
- it("handles strings containing commas, braces, and escapes", () => {
309
- const script = `frappe.query_reports["X"] = {
310
- filters: [
311
- { fieldname: "note", fieldtype: "Data", label: __("A, {B} \\"quoted\\"") },
312
- ],
313
- };`;
314
- const [filter] = extractScriptFilters(script).filters;
315
- expect(filter.label).toBe('A, {B} "quoted"');
316
- });
317
-
318
- it("drops Select/Link entries without options (unrenderable)", () => {
319
- const script = `frappe.query_reports["X"] = {
320
- filters: [{ fieldname: "pick", fieldtype: "Select" }],
321
- };`;
322
- const extraction = extractScriptFilters(script);
323
- expect(extraction.declaredCount).toBe(1);
324
- expect(extraction.filters).toHaveLength(0);
325
- });
326
- });
327
-
328
- describe("defaultFilterValues", () => {
329
- it("collects seedable defaults and skips empty ones", () => {
330
- expect(
331
- defaultFilterValues([
332
- { fieldname: "status", default: "Open" },
333
- { fieldname: "limit", default: 0 },
334
- { fieldname: "user", default: "" },
335
- { fieldname: "doctype" },
336
- ]),
337
- ).toEqual({ status: "Open", limit: 0 });
338
- });
339
- });
340
-
341
- describe("compactQueryReportFilters", () => {
342
- it("drops empty values and keeps hidden defaults so a visible clear-all is honest", () => {
343
- const meta = normalizeFilterMeta([
344
- { fieldname: "status", fieldtype: "Select", options: "Open\nClosed" },
345
- { fieldname: "owner", fieldtype: "Data" },
346
- { fieldname: "hidden_flag", fieldtype: "Check", hidden: 1, default: 1 },
347
- ]);
348
- expect(
349
- compactQueryReportFilters(meta, {
350
- status: "Open",
351
- owner: "",
352
- hidden_flag: 1,
353
- }),
354
- ).toEqual({ status: "Open", hidden_flag: 1 });
355
- });
356
- });
357
-
358
- describe("buildRunFilters", () => {
359
- const meta = normalizeFilterMeta([
360
- { fieldname: "search", fieldtype: "Data", wildcard_filter: 1 },
361
- { fieldname: "limit", fieldtype: "Int" },
362
- { fieldname: "show_permissions", fieldtype: "Check" },
363
- ]);
364
-
365
- it("omits empty and falsy values (desk get_filter_values parity)", () => {
366
- expect(
367
- buildRunFilters(meta, {
368
- search: "",
369
- limit: 0,
370
- show_permissions: false,
371
- user: null,
372
- doctype: undefined,
373
- }),
374
- ).toEqual({});
375
- });
376
-
377
- it("wraps wildcard filters and coerces numeric fieldtypes", () => {
378
- expect(buildRunFilters(meta, { search: "todo", limit: "25", show_permissions: true })).toEqual({
379
- search: "%todo%",
380
- limit: 25,
381
- show_permissions: 1,
382
- });
383
- });
384
-
385
- it("respects user-typed wildcards and passes unknown fieldnames through", () => {
386
- expect(buildRunFilters(meta, { search: "to%do", extra: "kept" })).toEqual({
387
- search: "to%do",
388
- extra: "kept",
389
- });
390
- });
391
- });