@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
package/src/i18n/en.ts CHANGED
@@ -272,6 +272,7 @@ export const en = {
272
272
  executionTime: "ran in {{seconds}}s",
273
273
  filtersRequiredDescription: "Set the required filter(s) to run this report: {{filters}}",
274
274
  filtersRequiredTitle: "Filters required",
275
+ notPermitted: 'You can\'t run "{{report}}"',
275
276
  refresh: "Refresh",
276
277
  retry: "Retry",
277
278
  rowCount: "{{count}} row(s)",
@@ -0,0 +1,134 @@
1
+ // The bulk actions bar offers Bulk Edit / Submit / Cancel / Delete only as
2
+ // far as the session's roles grant them on the doctype (MPO-428), measured
3
+ // against a live-shaped bench: the session RPCs for the user and roles, with
4
+ // meta (and its DocPerm rows) passed in as a prop, so the bar never asks for
5
+ // meta itself.
6
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
7
+ import { act, screen, waitFor, within } from "@testing-library/react";
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import { BackofficeProvider } from "../config/BackofficeProvider";
10
+ import { createMemoryNavigator } from "../navigation/navigator";
11
+ import type { DoctypeMeta } from "../shared/useDoctypeMeta";
12
+ import { ListBulkActionsBar } from "./ListBulkActionsBar";
13
+
14
+ const doctype = "QA Submittable";
15
+
16
+ const meta: DoctypeMeta = {
17
+ name: doctype,
18
+ is_submittable: 1,
19
+ fields: [{ fieldname: "subject", fieldtype: "Data", label: "Subject" }],
20
+ permissions: [
21
+ {
22
+ role: "Pokemon Editor",
23
+ read: 1,
24
+ write: 1,
25
+ create: 1,
26
+ delete: 1,
27
+ submit: 1,
28
+ cancel: 1,
29
+ },
30
+ { role: "Pokemon Writer", read: 1, write: 1, create: 1 },
31
+ { role: "Guest", read: 1 },
32
+ ],
33
+ };
34
+
35
+ let run = 0;
36
+ let roles: string[] = [];
37
+ let sessionAnswered = false;
38
+ let requested: string[] = [];
39
+
40
+ function json(body: unknown, status = 200) {
41
+ return new Response(JSON.stringify(body), {
42
+ status,
43
+ headers: { "content-type": "application/json" },
44
+ });
45
+ }
46
+
47
+ beforeEach(() => {
48
+ run += 1;
49
+ sessionAnswered = false;
50
+ requested = [];
51
+ vi.stubGlobal("fetch", async (input: string | URL | Request) => {
52
+ const url = new URL(String(input instanceof Request ? input.url : input));
53
+ const method = url.pathname.replace(/^\/api\/method\//, "");
54
+ requested.push(method);
55
+ if (method === "frappe.auth.get_logged_user") return json({ message: "user@pokemon.test" });
56
+ if (method === "frappe.core.doctype.user.user.get_roles") {
57
+ sessionAnswered = true;
58
+ return json({ message: roles });
59
+ }
60
+ return json({ data: [], message: [] });
61
+ });
62
+ });
63
+
64
+ afterEach(() => {
65
+ vi.restoreAllMocks();
66
+ vi.unstubAllGlobals();
67
+ });
68
+
69
+ function renderBar(barMeta: DoctypeMeta = meta) {
70
+ return renderWithProviders(
71
+ <BackofficeProvider
72
+ frappe={{ baseURL: `https://list-bulk-rights-${run}.spec.test` }}
73
+ navigation={createMemoryNavigator("/backoffice")}
74
+ >
75
+ <ListBulkActionsBar
76
+ doctype={barMeta.name}
77
+ meta={barMeta}
78
+ selectedNames={["SUB-001", "SUB-002"]}
79
+ onClearSelection={() => {}}
80
+ onActionComplete={() => {}}
81
+ />
82
+ </BackofficeProvider>,
83
+ );
84
+ }
85
+
86
+ async function settle() {
87
+ await waitFor(() => expect(sessionAnswered).toBe(true));
88
+ await act(async () => {
89
+ await new Promise((resolve) => setTimeout(resolve, 50));
90
+ });
91
+ }
92
+
93
+ function writeActionLabels(): string[] {
94
+ const root = within(document.body);
95
+ return ["Bulk Edit", "Submit", "Cancel", "Delete"].filter(
96
+ (label) => root.queryByRole("button", { name: label }) !== null,
97
+ );
98
+ }
99
+
100
+ describe("ListBulkActionsBar rights", () => {
101
+ it("offers an editor Bulk Edit, Submit, Cancel and Delete", async () => {
102
+ roles = ["Pokemon Editor", "Guest"];
103
+ renderBar();
104
+ await settle();
105
+ await waitFor(() =>
106
+ expect(writeActionLabels()).toEqual(["Bulk Edit", "Submit", "Cancel", "Delete"]),
107
+ );
108
+ expect(screen.getByText("2 selected")).toBeTruthy();
109
+ expect(requested.filter((method) => method.includes("getdoctype"))).toEqual([]);
110
+ });
111
+
112
+ it("offers a writer without delete Bulk Edit alone", async () => {
113
+ roles = ["Pokemon Writer", "Guest"];
114
+ renderBar();
115
+ await settle();
116
+ await waitFor(() => expect(writeActionLabels()).toEqual(["Bulk Edit"]));
117
+ });
118
+
119
+ it("keeps Submit and Cancel off a doctype that isn't submittable, even for an editor", async () => {
120
+ roles = ["Pokemon Editor", "Guest"];
121
+ renderBar({ ...meta, name: "QA Plain", is_submittable: 0 });
122
+ await settle();
123
+ await waitFor(() => expect(writeActionLabels()).toEqual(["Bulk Edit", "Delete"]));
124
+ });
125
+
126
+ it("offers a reader none of Bulk Edit, Submit, Cancel or Delete", async () => {
127
+ roles = ["Guest"];
128
+ renderBar();
129
+ await settle();
130
+ expect(writeActionLabels()).toEqual([]);
131
+ expect(screen.getByText("2 selected")).toBeTruthy();
132
+ expect(screen.getByRole("button", { name: "Export CSV" })).toBeTruthy();
133
+ });
134
+ });
@@ -1,6 +1,9 @@
1
1
  // ListBulkActionsBar over a selection (MPO-414): the bar's gates, its
2
2
  // confirm-guarded delete and docstatus RPCs, the Bulk Edit dialog it opens,
3
3
  // and the host list actions it offers for the doctype on screen.
4
+ // Fixture mode withholds no session rights (MPO-428); reader/editor coverage
5
+ // lives in ListBulkActionsBar.rights.spec.tsx.
6
+ import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
4
7
  import { renderWithProviders } from "@multiplatform.one/test-utils";
5
8
  import { fireEvent, screen, waitFor, within } from "@testing-library/react";
6
9
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -72,7 +75,10 @@ function renderBar({
72
75
  const onActionComplete = vi.fn();
73
76
  const view = renderWithProviders(
74
77
  <BackofficeProvider
75
- frappe={{ baseURL: `https://list-bulk-${run}.spec.test` }}
78
+ frappe={{
79
+ baseURL: `https://list-bulk-${run}.spec.test`,
80
+ fixtures: new InMemoryFixtureProvider({}),
81
+ }}
76
82
  navigation={createMemoryNavigator("/backoffice/ToDo")}
77
83
  capabilities={capabilities}
78
84
  actions={actions}
@@ -6,13 +6,13 @@
6
6
  // DataTable's built-in selection toolbar (house DG-TBL-02 pattern) using
7
7
  // the shared dialogs/helpers below; this standalone bar exists for surfaces
8
8
  // that manage selection themselves (report/kanban streams).
9
- import React from "react";
9
+ import React, { useMemo, useState } from "react";
10
10
  import { ConfirmDialog, Text, XStack, notify } from "@multiplatform.one/components";
11
11
  // Catalog action Button — `error` renders the destructive intent from the
12
12
  // semantic ramp instead of a hardcoded named hue (LC-05 ONE-EMPHASIS).
13
13
  import { Button } from "@multiplatform.one/forms";
14
+ import { sessionDocRights, useFrappeConfig } from "@multiplatform.one/frappe-ui";
14
15
  import { useResolvedKnobs } from "@multiplatform.one/theme";
15
- import { useState } from "react";
16
16
  import { useTranslation } from "react-i18next";
17
17
  import {
18
18
  actionsForDoctype,
@@ -22,6 +22,7 @@ import {
22
22
  import { BACKOFFICE_I18N_NAMESPACE } from "../i18n";
23
23
  import type { DoctypeMeta } from "../shared/useDoctypeMeta";
24
24
  import { useFrappeApp } from "../shared/useFrappeApp";
25
+ import { useUserRoles } from "../shared/useUserRoles";
25
26
  import { AddTagsDialog } from "./AddTagsDialog";
26
27
  import { AssignToDialog } from "./AssignToDialog";
27
28
  import { bulkDelete } from "./bulkActions";
@@ -51,6 +52,16 @@ export function ListBulkActionsBar({
51
52
  const app = useFrappeApp();
52
53
  const capabilities = useBackofficeCapabilities();
53
54
  const { actions } = useBackofficeConfig();
55
+ const config = useFrappeConfig();
56
+ const fixtureMode = Boolean(config?.fixtures);
57
+ // Session roles × meta.permissions: write/submit/cancel/delete bulk actions
58
+ // follow the doctype's DocPerm rows (DoctypeListScreen / MPO-390). Fixture
59
+ // mode has no session and withholds nothing.
60
+ const { roles, user: rolesUser } = useUserRoles();
61
+ const rights = useMemo(
62
+ () => sessionDocRights({ fixtures: fixtureMode, meta, roles, user: rolesUser }),
63
+ [fixtureMode, meta, roles, rolesUser],
64
+ );
54
65
  const [confirmDelete, setConfirmDelete] = useState(false);
55
66
  const [assignOpen, setAssignOpen] = useState(false);
56
67
  const [tagOpen, setTagOpen] = useState(false);
@@ -222,9 +233,11 @@ export function ListBulkActionsBar({
222
233
  count: selectedNames.length,
223
234
  })}
224
235
  </Text>
225
- <Button size={knobProps.sizeToken} onPress={() => setEditOpen(true)}>
226
- {t("list.editTitle", "Bulk Edit")}
227
- </Button>
236
+ {rights.write && (
237
+ <Button size={knobProps.sizeToken} onPress={() => setEditOpen(true)}>
238
+ {t("list.editTitle", "Bulk Edit")}
239
+ </Button>
240
+ )}
228
241
  <Button
229
242
  size={knobProps.sizeToken}
230
243
  disabled={exporting}
@@ -253,53 +266,57 @@ export function ListBulkActionsBar({
253
266
  {action.label}
254
267
  </Button>
255
268
  ))}
256
- {isSubmittable && (
257
- <>
258
- <Button
259
- size={knobProps.sizeToken}
260
- disabled={docstatusBusy}
261
- onPress={() => setDocstatusAction("submit")}
262
- >
263
- {t("list.submit", "Submit")}
264
- </Button>
265
- <Button
266
- size={knobProps.sizeToken}
267
- disabled={docstatusBusy}
268
- onPress={() => setDocstatusAction("cancel")}
269
- >
270
- {t("list.cancel", "Cancel")}
271
- </Button>
272
- </>
269
+ {isSubmittable && rights.submit && (
270
+ <Button
271
+ size={knobProps.sizeToken}
272
+ disabled={docstatusBusy}
273
+ onPress={() => setDocstatusAction("submit")}
274
+ >
275
+ {t("list.submit", "Submit")}
276
+ </Button>
277
+ )}
278
+ {isSubmittable && rights.cancel && (
279
+ <Button
280
+ size={knobProps.sizeToken}
281
+ disabled={docstatusBusy}
282
+ onPress={() => setDocstatusAction("cancel")}
283
+ >
284
+ {t("list.cancel", "Cancel")}
285
+ </Button>
273
286
  )}
274
287
  {/* Busy is derived state — `loading` shows the spinner and skips the
275
288
  DG-ST-03 bare-disabled guardrail (catalog Toolbar precedent). */}
276
- <Button
277
- size={knobProps.sizeToken}
278
- error
279
- loading={deleting}
280
- onPress={() => setConfirmDelete(true)}
281
- >
282
- {t("list.delete", "Delete")}
283
- </Button>
289
+ {rights.delete && (
290
+ <Button
291
+ size={knobProps.sizeToken}
292
+ error
293
+ loading={deleting}
294
+ onPress={() => setConfirmDelete(true)}
295
+ >
296
+ {t("list.delete", "Delete")}
297
+ </Button>
298
+ )}
284
299
  <Button size={knobProps.sizeToken} chromeless onPress={onClearSelection}>
285
300
  {t("list.clearSelection", "Clear selection")}
286
301
  </Button>
287
302
 
288
- <ConfirmDialog
289
- open={confirmDelete}
290
- onOpenChange={setConfirmDelete}
291
- destructive
292
- title={t("list.deleteTitle", "Delete documents")}
293
- body={t("list.deleteBody", {
294
- defaultValue: "Permanently delete {{count}} {{doctype}} document(s)?",
295
- count: selectedNames.length,
296
- doctype,
297
- })}
298
- confirmLabel={t("list.delete", "Delete")}
299
- onConfirm={() => {
300
- void handleDelete();
301
- }}
302
- />
303
+ {rights.delete && (
304
+ <ConfirmDialog
305
+ open={confirmDelete}
306
+ onOpenChange={setConfirmDelete}
307
+ destructive
308
+ title={t("list.deleteTitle", "Delete documents")}
309
+ body={t("list.deleteBody", {
310
+ defaultValue: "Permanently delete {{count}} {{doctype}} document(s)?",
311
+ count: selectedNames.length,
312
+ doctype,
313
+ })}
314
+ confirmLabel={t("list.delete", "Delete")}
315
+ onConfirm={() => {
316
+ void handleDelete();
317
+ }}
318
+ />
319
+ )}
303
320
  {showAssign && (
304
321
  <AssignToDialog
305
322
  doctype={doctype}
@@ -324,18 +341,20 @@ export function ListBulkActionsBar({
324
341
  }}
325
342
  />
326
343
  )}
327
- <BulkEditDialog
328
- doctype={doctype}
329
- meta={meta}
330
- names={selectedNames}
331
- open={editOpen}
332
- onOpenChange={setEditOpen}
333
- onDone={() => {
334
- onClearSelection();
335
- onActionComplete();
336
- }}
337
- />
338
- {isSubmittable && (
344
+ {rights.write && (
345
+ <BulkEditDialog
346
+ doctype={doctype}
347
+ meta={meta}
348
+ names={selectedNames}
349
+ open={editOpen}
350
+ onOpenChange={setEditOpen}
351
+ onDone={() => {
352
+ onClearSelection();
353
+ onActionComplete();
354
+ }}
355
+ />
356
+ )}
357
+ {isSubmittable && (rights.submit || rights.cancel) && (
339
358
  <ConfirmDialog
340
359
  open={docstatusAction !== undefined}
341
360
  onOpenChange={(open) => {
@@ -29,7 +29,7 @@ const todoMeta: DoctypeMeta = {
29
29
  };
30
30
 
31
31
  let run = 0;
32
- let requests: URL[] = [];
32
+ let savedFiltersRead: Promise<URL>;
33
33
 
34
34
  function json(body: unknown, status = 200) {
35
35
  return new Response(JSON.stringify(body), {
@@ -40,10 +40,18 @@ function json(body: unknown, status = 200) {
40
40
 
41
41
  beforeEach(() => {
42
42
  run += 1;
43
- requests = [];
43
+ let markSavedFiltersRead: (url: URL) => void = () => {};
44
+ savedFiltersRead = new Promise((resolve) => {
45
+ markSavedFiltersRead = resolve;
46
+ });
44
47
  vi.stubGlobal("fetch", async (input: string | URL | Request) => {
45
48
  const url = new URL(String(input instanceof Request ? input.url : input));
46
- requests.push(url);
49
+ if (
50
+ url.pathname.endsWith("frappe.client.get_list") &&
51
+ url.searchParams.get("doctype") === "List Filter"
52
+ ) {
53
+ markSavedFiltersRead(url);
54
+ }
47
55
  if (url.pathname.endsWith("frappe.auth.get_logged_user")) {
48
56
  return json({ message: "jane@example.com" });
49
57
  }
@@ -153,16 +161,9 @@ describe("ListFilterBar", () => {
153
161
 
154
162
  it("reads the doctype's saved filters and offers Save Filter only while filters apply", async () => {
155
163
  renderBar();
156
- await waitFor(() =>
157
- expect(
158
- requests.some(
159
- (url) =>
160
- url.pathname.endsWith("frappe.client.get_list") &&
161
- url.searchParams.get("doctype") === "List Filter" &&
162
- url.searchParams.get("filters") ===
163
- JSON.stringify([["reference_doctype", "=", "ToDo"]]),
164
- ),
165
- ).toBe(true),
164
+ const read = await savedFiltersRead;
165
+ expect(read.searchParams.get("filters")).toBe(
166
+ JSON.stringify([["reference_doctype", "=", "ToDo"]]),
166
167
  );
167
168
  expect(screen.queryByRole("button", { name: /save filter/i })).toBeNull();
168
169
  });
@@ -11,6 +11,7 @@ import {
11
11
  Skeleton,
12
12
  XStack,
13
13
  YStack,
14
+ useAccordionTriggerInk,
14
15
  } from "@multiplatform.one/components";
15
16
  import {
16
17
  GroupPositionContext,
@@ -34,6 +35,15 @@ export interface SidebarSectionProps {
34
35
  children: ReactNode;
35
36
  }
36
37
 
38
+ function SidebarSectionTitle({ title }: { title: string }) {
39
+ const color = useAccordionTriggerInk("$color11");
40
+ return (
41
+ <SizableText size="$2" fontWeight="400" color={color as never} numberOfLines={1} flexShrink={1}>
42
+ {title}
43
+ </SizableText>
44
+ );
45
+ }
46
+
37
47
  /** Trigger + facet stack. ListSidebar wraps this in Accordion.Item so the
38
48
  * Accordion root can count items (a custom Item wrapper is opaque). */
39
49
  export function SidebarSectionChrome({ title, children }: Omit<SidebarSectionProps, "value">) {
@@ -41,9 +51,7 @@ export function SidebarSectionChrome({ title, children }: Omit<SidebarSectionPro
41
51
  return (
42
52
  <>
43
53
  <Accordion.Trigger minHeight="$4" paddingVertical="$1.5" paddingHorizontal="$2">
44
- <SizableText size="$2" fontWeight="400" color="$color11" numberOfLines={1} flexShrink={1}>
45
- {title}
46
- </SizableText>
54
+ <SidebarSectionTitle title={title} />
47
55
  </Accordion.Trigger>
48
56
  <Accordion.Content padding={0}>
49
57
  <YStack>