@multiplatform.one/frappe-ui 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 (325) hide show
  1. package/dist/cjs/FrappeCalendar.cjs +3 -2
  2. package/dist/cjs/FrappeCalendar.native.js +3 -2
  3. package/dist/cjs/FrappeCalendar.native.js.map +1 -1
  4. package/dist/cjs/FrappeForm.cjs +148 -464
  5. package/dist/cjs/FrappeForm.native.js +152 -633
  6. package/dist/cjs/FrappeForm.native.js.map +1 -1
  7. package/dist/cjs/FrappeListView.cjs +5 -27
  8. package/dist/cjs/FrappeListView.native.js +11 -32
  9. package/dist/cjs/FrappeListView.native.js.map +1 -1
  10. package/dist/cjs/FrappeReportBuilder.cjs +523 -309
  11. package/dist/cjs/FrappeReportBuilder.native.js +658 -353
  12. package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
  13. package/dist/cjs/FrappeReportView.cjs +35 -234
  14. package/dist/cjs/FrappeReportView.native.js +39 -283
  15. package/dist/cjs/FrappeReportView.native.js.map +1 -1
  16. package/dist/cjs/dependsOn.cjs +2 -2
  17. package/dist/cjs/dependsOn.native.js +1 -2
  18. package/dist/cjs/dependsOn.native.js.map +1 -1
  19. package/dist/cjs/fields/shared/EditorPreviewPanes.cjs +2 -2
  20. package/dist/cjs/fields/shared/EditorPreviewPanes.native.js +2 -2
  21. package/dist/cjs/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  22. package/dist/cjs/formEngine/docPermissions.cjs +20 -0
  23. package/dist/cjs/formEngine/docPermissions.native.js +69 -0
  24. package/dist/cjs/formEngine/docPermissions.native.js.map +1 -1
  25. package/dist/cjs/formEngine/index.cjs +2 -0
  26. package/dist/cjs/formEngine/index.native.js +2 -0
  27. package/dist/cjs/formEngine/index.native.js.map +1 -1
  28. package/dist/cjs/index.cjs +67 -7
  29. package/dist/cjs/index.native.js +67 -7
  30. package/dist/cjs/index.native.js.map +1 -1
  31. package/dist/cjs/liveErrors.cjs +8 -2
  32. package/dist/cjs/liveErrors.native.js +8 -2
  33. package/dist/cjs/liveErrors.native.js.map +1 -1
  34. package/dist/cjs/measures.cjs +31 -0
  35. package/dist/cjs/measures.native.js +34 -0
  36. package/dist/cjs/measures.native.js.map +1 -0
  37. package/dist/cjs/recordShape.cjs +103 -0
  38. package/dist/cjs/recordShape.native.js +115 -0
  39. package/dist/cjs/recordShape.native.js.map +1 -0
  40. package/dist/cjs/report/ReportGrid.cjs +453 -0
  41. package/dist/cjs/report/ReportGrid.native.js +510 -0
  42. package/dist/cjs/report/ReportGrid.native.js.map +1 -0
  43. package/dist/cjs/report/ReportViewToolbar.cjs +253 -0
  44. package/dist/cjs/report/ReportViewToolbar.native.js +303 -0
  45. package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -0
  46. package/dist/cjs/report/queryReport.cjs +444 -0
  47. package/dist/cjs/report/queryReport.native.js +588 -0
  48. package/dist/cjs/report/queryReport.native.js.map +1 -0
  49. package/dist/cjs/report/queryReportColumns.cjs +131 -0
  50. package/dist/cjs/report/queryReportColumns.native.js +159 -0
  51. package/dist/cjs/report/queryReportColumns.native.js.map +1 -0
  52. package/dist/cjs/report/reportViewModel.cjs +386 -0
  53. package/dist/cjs/report/reportViewModel.native.js +657 -0
  54. package/dist/cjs/report/reportViewModel.native.js.map +1 -0
  55. package/dist/cjs/report/savedReports.cjs +137 -0
  56. package/dist/cjs/report/savedReports.native.js +169 -0
  57. package/dist/cjs/report/savedReports.native.js.map +1 -0
  58. package/dist/cjs/report/useFrappeReport.cjs +184 -0
  59. package/dist/cjs/report/useFrappeReport.native.js +214 -0
  60. package/dist/cjs/report/useFrappeReport.native.js.map +1 -0
  61. package/dist/cjs/report/useQueryReport.cjs +201 -0
  62. package/dist/cjs/report/useQueryReport.native.js +233 -0
  63. package/dist/cjs/report/useQueryReport.native.js.map +1 -0
  64. package/dist/cjs/useFrappeForm.cjs +558 -0
  65. package/dist/cjs/useFrappeForm.native.js +714 -0
  66. package/dist/cjs/useFrappeForm.native.js.map +1 -0
  67. package/dist/esm/FrappeCalendar.mjs +4 -3
  68. package/dist/esm/FrappeCalendar.mjs.map +1 -1
  69. package/dist/esm/FrappeCalendar.native.js +4 -3
  70. package/dist/esm/FrappeCalendar.native.js.map +1 -1
  71. package/dist/esm/FrappeForm.mjs +151 -467
  72. package/dist/esm/FrappeForm.mjs.map +1 -1
  73. package/dist/esm/FrappeForm.native.js +155 -636
  74. package/dist/esm/FrappeForm.native.js.map +1 -1
  75. package/dist/esm/FrappeListView.mjs +6 -28
  76. package/dist/esm/FrappeListView.mjs.map +1 -1
  77. package/dist/esm/FrappeListView.native.js +12 -33
  78. package/dist/esm/FrappeListView.native.js.map +1 -1
  79. package/dist/esm/FrappeReportBuilder.mjs +526 -312
  80. package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
  81. package/dist/esm/FrappeReportBuilder.native.js +661 -356
  82. package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
  83. package/dist/esm/FrappeReportView.mjs +30 -229
  84. package/dist/esm/FrappeReportView.mjs.map +1 -1
  85. package/dist/esm/FrappeReportView.native.js +34 -278
  86. package/dist/esm/FrappeReportView.native.js.map +1 -1
  87. package/dist/esm/dependsOn.mjs +2 -2
  88. package/dist/esm/dependsOn.mjs.map +1 -1
  89. package/dist/esm/dependsOn.native.js +1 -2
  90. package/dist/esm/dependsOn.native.js.map +1 -1
  91. package/dist/esm/fields/shared/EditorPreviewPanes.mjs +2 -2
  92. package/dist/esm/fields/shared/EditorPreviewPanes.mjs.map +1 -1
  93. package/dist/esm/fields/shared/EditorPreviewPanes.native.js +2 -2
  94. package/dist/esm/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  95. package/dist/esm/formEngine/docPermissions.mjs +19 -1
  96. package/dist/esm/formEngine/docPermissions.mjs.map +1 -1
  97. package/dist/esm/formEngine/docPermissions.native.js +68 -1
  98. package/dist/esm/formEngine/docPermissions.native.js.map +1 -1
  99. package/dist/esm/formEngine/index.mjs +2 -2
  100. package/dist/esm/formEngine/index.mjs.map +1 -1
  101. package/dist/esm/formEngine/index.native.js +2 -2
  102. package/dist/esm/formEngine/index.native.js.map +1 -1
  103. package/dist/esm/index.mjs +16 -4
  104. package/dist/esm/index.mjs.map +1 -1
  105. package/dist/esm/index.native.js +16 -4
  106. package/dist/esm/index.native.js.map +1 -1
  107. package/dist/esm/liveErrors.mjs +8 -3
  108. package/dist/esm/liveErrors.mjs.map +1 -1
  109. package/dist/esm/liveErrors.native.js +8 -3
  110. package/dist/esm/liveErrors.native.js.map +1 -1
  111. package/dist/esm/measures.mjs +5 -0
  112. package/dist/esm/measures.mjs.map +1 -0
  113. package/dist/esm/measures.native.js +5 -0
  114. package/dist/esm/measures.native.js.map +1 -0
  115. package/dist/esm/recordShape.mjs +74 -0
  116. package/dist/esm/recordShape.mjs.map +1 -0
  117. package/dist/esm/recordShape.native.js +83 -0
  118. package/dist/esm/recordShape.native.js.map +1 -0
  119. package/dist/esm/report/ReportGrid.mjs +428 -0
  120. package/dist/esm/report/ReportGrid.mjs.map +1 -0
  121. package/dist/esm/report/ReportGrid.native.js +471 -0
  122. package/dist/esm/report/ReportGrid.native.js.map +1 -0
  123. package/dist/esm/report/ReportViewToolbar.mjs +228 -0
  124. package/dist/esm/report/ReportViewToolbar.mjs.map +1 -0
  125. package/dist/esm/report/ReportViewToolbar.native.js +264 -0
  126. package/dist/esm/report/ReportViewToolbar.native.js.map +1 -0
  127. package/dist/esm/report/queryReport.mjs +404 -0
  128. package/dist/esm/report/queryReport.mjs.map +1 -0
  129. package/dist/esm/report/queryReport.native.js +545 -0
  130. package/dist/esm/report/queryReport.native.js.map +1 -0
  131. package/dist/esm/report/queryReportColumns.mjs +105 -0
  132. package/dist/esm/report/queryReportColumns.mjs.map +1 -0
  133. package/dist/esm/report/queryReportColumns.native.js +119 -0
  134. package/dist/esm/report/queryReportColumns.native.js.map +1 -0
  135. package/dist/esm/report/reportViewModel.mjs +343 -0
  136. package/dist/esm/report/reportViewModel.mjs.map +1 -0
  137. package/dist/esm/report/reportViewModel.native.js +611 -0
  138. package/dist/esm/report/reportViewModel.native.js.map +1 -0
  139. package/dist/esm/report/savedReports.mjs +112 -0
  140. package/dist/esm/report/savedReports.mjs.map +1 -0
  141. package/dist/esm/report/savedReports.native.js +141 -0
  142. package/dist/esm/report/savedReports.native.js.map +1 -0
  143. package/dist/esm/report/useFrappeReport.mjs +158 -0
  144. package/dist/esm/report/useFrappeReport.mjs.map +1 -0
  145. package/dist/esm/report/useFrappeReport.native.js +185 -0
  146. package/dist/esm/report/useFrappeReport.native.js.map +1 -0
  147. package/dist/esm/report/useQueryReport.mjs +176 -0
  148. package/dist/esm/report/useQueryReport.mjs.map +1 -0
  149. package/dist/esm/report/useQueryReport.native.js +205 -0
  150. package/dist/esm/report/useQueryReport.native.js.map +1 -0
  151. package/dist/esm/useFrappeForm.mjs +533 -0
  152. package/dist/esm/useFrappeForm.mjs.map +1 -0
  153. package/dist/esm/useFrappeForm.native.js +686 -0
  154. package/dist/esm/useFrappeForm.native.js.map +1 -0
  155. package/dist/jsx/FrappeCalendar.mjs +4 -3
  156. package/dist/jsx/FrappeCalendar.mjs.map +1 -1
  157. package/dist/jsx/FrappeCalendar.native.js +3 -2
  158. package/dist/jsx/FrappeCalendar.native.js.map +1 -1
  159. package/dist/jsx/FrappeForm.mjs +151 -467
  160. package/dist/jsx/FrappeForm.mjs.map +1 -1
  161. package/dist/jsx/FrappeForm.native.js +152 -633
  162. package/dist/jsx/FrappeForm.native.js.map +1 -1
  163. package/dist/jsx/FrappeListView.mjs +6 -28
  164. package/dist/jsx/FrappeListView.mjs.map +1 -1
  165. package/dist/jsx/FrappeListView.native.js +11 -32
  166. package/dist/jsx/FrappeListView.native.js.map +1 -1
  167. package/dist/jsx/FrappeReportBuilder.mjs +526 -312
  168. package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
  169. package/dist/jsx/FrappeReportBuilder.native.js +658 -353
  170. package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
  171. package/dist/jsx/FrappeReportView.mjs +30 -229
  172. package/dist/jsx/FrappeReportView.mjs.map +1 -1
  173. package/dist/jsx/FrappeReportView.native.js +39 -283
  174. package/dist/jsx/FrappeReportView.native.js.map +1 -1
  175. package/dist/jsx/dependsOn.mjs +2 -2
  176. package/dist/jsx/dependsOn.mjs.map +1 -1
  177. package/dist/jsx/dependsOn.native.js +1 -2
  178. package/dist/jsx/dependsOn.native.js.map +1 -1
  179. package/dist/jsx/fields/shared/EditorPreviewPanes.mjs +2 -2
  180. package/dist/jsx/fields/shared/EditorPreviewPanes.mjs.map +1 -1
  181. package/dist/jsx/fields/shared/EditorPreviewPanes.native.js +2 -2
  182. package/dist/jsx/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  183. package/dist/jsx/formEngine/docPermissions.mjs +19 -1
  184. package/dist/jsx/formEngine/docPermissions.mjs.map +1 -1
  185. package/dist/jsx/formEngine/docPermissions.native.js +69 -0
  186. package/dist/jsx/formEngine/docPermissions.native.js.map +1 -1
  187. package/dist/jsx/formEngine/index.mjs +2 -2
  188. package/dist/jsx/formEngine/index.mjs.map +1 -1
  189. package/dist/jsx/formEngine/index.native.js +2 -0
  190. package/dist/jsx/formEngine/index.native.js.map +1 -1
  191. package/dist/jsx/index.js +16 -4
  192. package/dist/jsx/index.js.map +1 -1
  193. package/dist/jsx/index.mjs +16 -4
  194. package/dist/jsx/index.mjs.map +1 -1
  195. package/dist/jsx/index.native.js +67 -7
  196. package/dist/jsx/index.native.js.map +1 -1
  197. package/dist/jsx/liveErrors.mjs +8 -3
  198. package/dist/jsx/liveErrors.mjs.map +1 -1
  199. package/dist/jsx/liveErrors.native.js +8 -2
  200. package/dist/jsx/liveErrors.native.js.map +1 -1
  201. package/dist/jsx/measures.mjs +5 -0
  202. package/dist/jsx/measures.mjs.map +1 -0
  203. package/dist/jsx/measures.native.js +34 -0
  204. package/dist/jsx/measures.native.js.map +1 -0
  205. package/dist/jsx/recordShape.mjs +74 -0
  206. package/dist/jsx/recordShape.mjs.map +1 -0
  207. package/dist/jsx/recordShape.native.js +115 -0
  208. package/dist/jsx/recordShape.native.js.map +1 -0
  209. package/dist/jsx/report/ReportGrid.mjs +428 -0
  210. package/dist/jsx/report/ReportGrid.mjs.map +1 -0
  211. package/dist/jsx/report/ReportGrid.native.js +510 -0
  212. package/dist/jsx/report/ReportGrid.native.js.map +1 -0
  213. package/dist/jsx/report/ReportViewToolbar.mjs +228 -0
  214. package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -0
  215. package/dist/jsx/report/ReportViewToolbar.native.js +303 -0
  216. package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -0
  217. package/dist/jsx/report/queryReport.mjs +404 -0
  218. package/dist/jsx/report/queryReport.mjs.map +1 -0
  219. package/dist/jsx/report/queryReport.native.js +588 -0
  220. package/dist/jsx/report/queryReport.native.js.map +1 -0
  221. package/dist/jsx/report/queryReportColumns.mjs +105 -0
  222. package/dist/jsx/report/queryReportColumns.mjs.map +1 -0
  223. package/dist/jsx/report/queryReportColumns.native.js +159 -0
  224. package/dist/jsx/report/queryReportColumns.native.js.map +1 -0
  225. package/dist/jsx/report/reportViewModel.mjs +343 -0
  226. package/dist/jsx/report/reportViewModel.mjs.map +1 -0
  227. package/dist/jsx/report/reportViewModel.native.js +657 -0
  228. package/dist/jsx/report/reportViewModel.native.js.map +1 -0
  229. package/dist/jsx/report/savedReports.mjs +112 -0
  230. package/dist/jsx/report/savedReports.mjs.map +1 -0
  231. package/dist/jsx/report/savedReports.native.js +169 -0
  232. package/dist/jsx/report/savedReports.native.js.map +1 -0
  233. package/dist/jsx/report/useFrappeReport.mjs +158 -0
  234. package/dist/jsx/report/useFrappeReport.mjs.map +1 -0
  235. package/dist/jsx/report/useFrappeReport.native.js +214 -0
  236. package/dist/jsx/report/useFrappeReport.native.js.map +1 -0
  237. package/dist/jsx/report/useQueryReport.mjs +176 -0
  238. package/dist/jsx/report/useQueryReport.mjs.map +1 -0
  239. package/dist/jsx/report/useQueryReport.native.js +233 -0
  240. package/dist/jsx/report/useQueryReport.native.js.map +1 -0
  241. package/dist/jsx/useFrappeForm.mjs +533 -0
  242. package/dist/jsx/useFrappeForm.mjs.map +1 -0
  243. package/dist/jsx/useFrappeForm.native.js +714 -0
  244. package/dist/jsx/useFrappeForm.native.js.map +1 -0
  245. package/package.json +16 -16
  246. package/src/FrappeCalendar.tsx +8 -2
  247. package/src/FrappeForm.stories.tsx +52 -0
  248. package/src/FrappeForm.tsx +176 -565
  249. package/src/FrappeListView.cards.stories.tsx +3 -6
  250. package/src/FrappeListView.tsx +15 -55
  251. package/src/FrappeReportBuilder.stories.tsx +34 -7
  252. package/src/FrappeReportBuilder.tsx +790 -380
  253. package/src/FrappeReportView.spec.tsx +10 -6
  254. package/src/FrappeReportView.stories.tsx +20 -1
  255. package/src/FrappeReportView.tsx +89 -415
  256. package/src/dependsOn.ts +4 -1
  257. package/src/fields/shared/EditorPreviewPanes.tsx +2 -2
  258. package/src/formEngine/docPermissions.spec.ts +36 -1
  259. package/src/formEngine/docPermissions.ts +34 -0
  260. package/src/formEngine/index.ts +8 -1
  261. package/src/frappeRemainder.designLaw.spec.ts +9 -1
  262. package/src/frappeRoutes.spec.tsx +9 -0
  263. package/src/frappeViews.designLaw.spec.ts +15 -2
  264. package/src/guestFallback.spec.tsx +236 -0
  265. package/src/index.ts +117 -16
  266. package/src/liveErrors.spec.ts +10 -0
  267. package/src/liveErrors.ts +16 -3
  268. package/src/measures.ts +11 -0
  269. package/src/recordShape.spec.ts +100 -0
  270. package/src/recordShape.ts +133 -0
  271. package/src/report/ReportGrid.spec.tsx +175 -0
  272. package/src/report/ReportGrid.tsx +530 -0
  273. package/src/report/ReportViewToolbar.spec.tsx +90 -0
  274. package/src/report/ReportViewToolbar.tsx +314 -0
  275. package/src/report/queryReport.spec.ts +483 -0
  276. package/src/report/queryReport.ts +722 -0
  277. package/src/report/queryReportColumns.tsx +123 -0
  278. package/src/report/reportViewModel.spec.ts +393 -0
  279. package/src/report/reportViewModel.ts +617 -0
  280. package/src/report/savedReports.ts +172 -0
  281. package/src/report/seam.spec.ts +216 -0
  282. package/src/report/useFrappeReport.ts +241 -0
  283. package/src/report/useQueryReport.spec.tsx +131 -0
  284. package/src/report/useQueryReport.ts +309 -0
  285. package/src/useFrappeForm.spec.tsx +209 -0
  286. package/src/useFrappeForm.ts +778 -0
  287. package/types/FrappeCalendar.d.ts.map +1 -1
  288. package/types/FrappeForm.d.ts +21 -1
  289. package/types/FrappeForm.d.ts.map +1 -1
  290. package/types/FrappeListView.d.ts.map +1 -1
  291. package/types/FrappeReportBuilder.d.ts +66 -16
  292. package/types/FrappeReportBuilder.d.ts.map +1 -1
  293. package/types/FrappeReportView.d.ts +9 -127
  294. package/types/FrappeReportView.d.ts.map +1 -1
  295. package/types/dependsOn.d.ts.map +1 -1
  296. package/types/formEngine/docPermissions.d.ts +14 -0
  297. package/types/formEngine/docPermissions.d.ts.map +1 -1
  298. package/types/formEngine/index.d.ts +1 -1
  299. package/types/formEngine/index.d.ts.map +1 -1
  300. package/types/index.d.ts +15 -3
  301. package/types/index.d.ts.map +1 -1
  302. package/types/liveErrors.d.ts +9 -1
  303. package/types/liveErrors.d.ts.map +1 -1
  304. package/types/measures.d.ts +3 -0
  305. package/types/measures.d.ts.map +1 -0
  306. package/types/recordShape.d.ts +45 -0
  307. package/types/recordShape.d.ts.map +1 -0
  308. package/types/report/ReportGrid.d.ts +20 -0
  309. package/types/report/ReportGrid.d.ts.map +1 -0
  310. package/types/report/ReportViewToolbar.d.ts +31 -0
  311. package/types/report/ReportViewToolbar.d.ts.map +1 -0
  312. package/types/report/queryReport.d.ts +181 -0
  313. package/types/report/queryReport.d.ts.map +1 -0
  314. package/types/report/queryReportColumns.d.ts +12 -0
  315. package/types/report/queryReportColumns.d.ts.map +1 -0
  316. package/types/report/reportViewModel.d.ts +146 -0
  317. package/types/report/reportViewModel.d.ts.map +1 -0
  318. package/types/report/savedReports.d.ts +27 -0
  319. package/types/report/savedReports.d.ts.map +1 -0
  320. package/types/report/useFrappeReport.d.ts +60 -0
  321. package/types/report/useFrappeReport.d.ts.map +1 -0
  322. package/types/report/useQueryReport.d.ts +37 -0
  323. package/types/report/useQueryReport.d.ts.map +1 -0
  324. package/types/useFrappeForm.d.ts +142 -0
  325. package/types/useFrappeForm.d.ts.map +1 -0
@@ -3,7 +3,14 @@
3
3
  // get_role_permissions: permlevel 0 only, OR across the user's roles,
4
4
  // if_owner rows scoped to the user's own records except create.
5
5
  import { describe, expect, it } from "vitest";
6
- import { allDocRights, resolveDocRights, sessionDocRights, type DocRights } from "./docPermissions";
6
+ import {
7
+ allDocRights,
8
+ docinfoRights,
9
+ fixtureDocRights,
10
+ resolveDocRights,
11
+ sessionDocRights,
12
+ type DocRights,
13
+ } from "./docPermissions";
7
14
  import type { DocPermissionRule } from "./doctypeMeta";
8
15
 
9
16
  const pokeMove: DocPermissionRule[] = [
@@ -143,3 +150,31 @@ describe("sessionDocRights", () => {
143
150
  expect([rights.create, rights.write, rights.delete]).toEqual([true, true, true]);
144
151
  });
145
152
  });
153
+
154
+ describe("docinfoRights", () => {
155
+ it("takes the bench's per-record answer as it is", () => {
156
+ const rights = docinfoRights({ read: 1, write: 1, submit: 0, print: 1 });
157
+ expect(rights && granted(rights)).toEqual(["print", "read", "write"]);
158
+ });
159
+
160
+ it("is no answer when the payload carries no read key (token-auth {null: 0})", () => {
161
+ expect(docinfoRights({ null: 0 })).toBeUndefined();
162
+ expect(docinfoRights(undefined)).toBeUndefined();
163
+ });
164
+ });
165
+
166
+ describe("fixtureDocRights", () => {
167
+ it("grants what any permlevel-0 row grants, never a higher level's rights", () => {
168
+ const rights = fixtureDocRights({ permissions: pokeMove });
169
+ expect(granted(rights)).toEqual(["create", "delete", "export", "read", "write"]);
170
+ });
171
+
172
+ it("keeps write, create, email and print for a doctype without rows", () => {
173
+ expect(granted(fixtureDocRights({ permissions: [] }))).toEqual([
174
+ "create",
175
+ "email",
176
+ "print",
177
+ "write",
178
+ ]);
179
+ });
180
+ });
@@ -126,3 +126,37 @@ export function sessionDocRights({
126
126
  if (!meta || !roles) return allDocRights(false);
127
127
  return resolveDocRights({ permissions: meta.permissions, roles, user, owner, shares });
128
128
  }
129
+
130
+ /**
131
+ * A record's rights as the bench evaluated them for this user: docinfo's
132
+ * `permissions` (desk `frm.perm[0]` from getdoc). A payload with no `read`
133
+ * key is not an answer (a token-auth bench sends `{"null": 0}`), so it gives
134
+ * undefined and the caller keeps the session's rights.
135
+ */
136
+ export function docinfoRights(
137
+ permissions: Readonly<Record<string, unknown>> | null | undefined,
138
+ ): DocRights | undefined {
139
+ if (!permissions || !("read" in permissions)) return undefined;
140
+ const rights = allDocRights(false);
141
+ for (const right of docRightNames) rights[right] = !!permissions[right];
142
+ return rights;
143
+ }
144
+
145
+ /**
146
+ * The desk's rights on a fixtures connection, where no session exists: what
147
+ * any permlevel-0 DocPerm row of the doctype grants, so a story offers the
148
+ * actions its meta allows. A doctype without rows keeps write, create,
149
+ * email and print.
150
+ */
151
+ export function fixtureDocRights(meta: Pick<DoctypeMeta, "permissions"> | undefined): DocRights {
152
+ const rows = (meta?.permissions ?? []).filter((row) => !(Number(row.permlevel ?? 0) || 0));
153
+ const rights = allDocRights(false);
154
+ for (const right of docRightNames) rights[right] = rows.some((row) => !!row[right]);
155
+ if ((meta?.permissions ?? []).length === 0) {
156
+ rights.write = true;
157
+ rights.create = true;
158
+ rights.email = true;
159
+ rights.print = true;
160
+ }
161
+ return rights;
162
+ }
@@ -35,7 +35,14 @@ export type { FormEngine } from "./useFormEngine";
35
35
  export { useFrappeSession, useSessionRoles } from "./useSessionRoles";
36
36
  export type { FrappeSession } from "./useSessionRoles";
37
37
 
38
- export { allDocRights, docRightNames, resolveDocRights, sessionDocRights } from "./docPermissions";
38
+ export {
39
+ allDocRights,
40
+ docinfoRights,
41
+ docRightNames,
42
+ fixtureDocRights,
43
+ resolveDocRights,
44
+ sessionDocRights,
45
+ } from "./docPermissions";
39
46
  export type {
40
47
  DocRight,
41
48
  DocRights,
@@ -102,7 +102,15 @@ describe("Frappe remainder wrappers — design-law 100%", () => {
102
102
  });
103
103
 
104
104
  it("FrappeReportBuilder uses house Button + PopoverContent and R7 weight 400", () => {
105
- const source = read("FrappeReportBuilder.tsx");
105
+ // The builder composes its toolbar and grid (MPO-15, GL-28); the law
106
+ // holds over the three files together.
107
+ const source = [
108
+ "FrappeReportBuilder.tsx",
109
+ "report/ReportViewToolbar.tsx",
110
+ "report/ReportGrid.tsx",
111
+ ]
112
+ .map(read)
113
+ .join("\n");
106
114
  expect(source).toContain("PopoverContent");
107
115
  expect(source).toContain('from "@multiplatform.one/forms"');
108
116
  expect(source).toContain("Button");
@@ -69,6 +69,8 @@ let writes: Write[] = [];
69
69
  let guest = false;
70
70
  let session: { user: string; roles: string[] } | null = editor;
71
71
  let sessionAnswered = false;
72
+ let movesListed: Promise<void> = Promise.resolve();
73
+ let markMovesListed = () => {};
72
74
  let confirmDelete = vi.fn(() => true);
73
75
 
74
76
  function json(body: unknown, status = 200) {
@@ -115,6 +117,7 @@ function stubBench() {
115
117
  const rows = [...moves.values()].filter((row) =>
116
118
  matchesNameFilter(row, url.searchParams.get("filters")),
117
119
  );
120
+ if (url.searchParams.get("fields")?.includes('"move_name"')) markMovesListed();
118
121
  return json({ data: rows });
119
122
  }
120
123
  if (url.pathname.startsWith(`${resource}/`)) {
@@ -209,6 +212,9 @@ beforeEach(() => {
209
212
  guest = false;
210
213
  session = editor;
211
214
  sessionAnswered = false;
215
+ movesListed = new Promise((resolve) => {
216
+ markMovesListed = resolve;
217
+ });
212
218
  writes = [];
213
219
  moves = new Map([
214
220
  [
@@ -283,6 +289,9 @@ describe("FrappeListView with a route", () => {
283
289
  it("offers no Add to a reader who cannot load the doctype's meta", async () => {
284
290
  guest = true;
285
291
  const { container } = renderAt("/moves", "list");
292
+ // A 403 on meta costs two more round trips before a row can show: a
293
+ // one-doc peek, then the re-read with its keys. Wait for that read.
294
+ await movesListed;
286
295
  await waitFor(() => expect(screen.getByText("tackle")).toBeTruthy());
287
296
  expect(() => button(container, `Add ${doctype}`)).toThrow();
288
297
  });
@@ -75,11 +75,24 @@ describe("Frappe view adapters — design-law 100%", () => {
75
75
  expect(source).not.toMatch(/fontSize="\$1"|fontSize="\$2"/);
76
76
  });
77
77
 
78
+ it("report view toolbar filters are catalog Chip, not 999-radius wells", () => {
79
+ const source = read("report/ReportViewToolbar.tsx");
80
+ expect(source).toContain("<Chip");
81
+ expect(source).not.toMatch(/borderRadius=\{999\}/);
82
+ });
83
+
78
84
  it("FrappeReportBuilder toolbar is house Button and captions lock 400 (R7 / LC-54)", () => {
79
- const source = read("FrappeReportBuilder.tsx");
85
+ // The builder composes its toolbar and grid (MPO-15, GL-28); the law
86
+ // holds over the three files together.
87
+ const source = [
88
+ "FrappeReportBuilder.tsx",
89
+ "report/ReportViewToolbar.tsx",
90
+ "report/ReportGrid.tsx",
91
+ ]
92
+ .map(read)
93
+ .join("\n");
80
94
  expect(source).toMatch(/import \{[^}]*Button[^}]*\} from "@multiplatform.one\/forms"/);
81
95
  expect(source).not.toMatch(/Button.*from "tamagui"/);
82
- expect(source).toContain("formCommonColors");
83
96
  expect(source).not.toMatch(/fontWeight="600"|fontWeight="500"/);
84
97
  expect(source).toContain('fontWeight="400"');
85
98
  expect(source).not.toMatch(/<Button\b[^>]*\b(?:controlFill|backgroundColor|borderColor)=/);
@@ -0,0 +1,236 @@
1
+ // LC-96 (MPO-15 AC10): a data-aware view degrades to the weakest honest
2
+ // rendering, never a blank. The bench below refuses getdoctype the way Frappe
3
+ // refuses it to Guest, answers or refuses the record reads per test, and
4
+ // refuses or breaks a query report's run. Each rung of the ladder is one test:
5
+ // meta refused but the rows readable, the record refused too, nothing to
6
+ // create from, and a read that failed for some other reason, for the form, the
7
+ // list and both report views.
8
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
9
+ import { cleanup, screen, waitFor } from "@testing-library/react";
10
+ import i18next from "i18next";
11
+ import { initReactI18next } from "react-i18next";
12
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
13
+ import { FrappeForm } from "./FrappeForm";
14
+ import { FrappeListView } from "./FrappeListView";
15
+ import { frappeRouteTarget, type FrappeNavigation } from "./frappeNavigation";
16
+ import { FrappeProvider } from "./FrappeProvider";
17
+ import { FrappeReportBuilder } from "./FrappeReportBuilder";
18
+ import { FrappeReportView } from "./FrappeReportView";
19
+
20
+ const doctype = "Poke Move";
21
+ const route = "/moves";
22
+
23
+ interface Move {
24
+ name: string;
25
+ doctype: string;
26
+ move_name?: string;
27
+ power?: number;
28
+ }
29
+
30
+ const tackle = {
31
+ name: "a1b2c3",
32
+ doctype,
33
+ owner: "Administrator",
34
+ modified: "2026-09-29 10:00:00",
35
+ docstatus: 0,
36
+ idx: 0,
37
+ _user_tags: null,
38
+ move_name: "tackle",
39
+ power: 40,
40
+ accuracy: 0.95,
41
+ };
42
+
43
+ const settle = { timeout: 8000 };
44
+
45
+ let run = 0;
46
+ let bench = "";
47
+ let metaAnswer: "refused" | "broken" = "refused";
48
+ let recordsReadable = true;
49
+ let reportRun: "refused" | "broken" = "refused";
50
+ const reportName = "Move Usage";
51
+
52
+ function json(body: unknown, status = 200) {
53
+ return new Response(JSON.stringify(body), {
54
+ status,
55
+ headers: { "content-type": "application/json" },
56
+ });
57
+ }
58
+
59
+ function stubBench() {
60
+ vi.stubGlobal("fetch", async (input: string | URL | Request) => {
61
+ const url = new URL(String(input instanceof Request ? input.url : input));
62
+ const method = url.pathname.replace(/^\/api\/method\//, "");
63
+ if (method === "frappe.desk.form.load.getdoctype") {
64
+ return metaAnswer === "refused"
65
+ ? json({ exc_type: "PermissionError", exception: "Not permitted" }, 403)
66
+ : json({ exc_type: "ValidationError", exception: "Poke Move is being migrated" }, 417);
67
+ }
68
+ if (method === "frappe.desk.query_report.run") {
69
+ return reportRun === "refused"
70
+ ? json(
71
+ {
72
+ exc_type: "PermissionError",
73
+ _server_messages: JSON.stringify([
74
+ JSON.stringify({ message: `You don't have access to Report: ${reportName}` }),
75
+ ]),
76
+ },
77
+ 403,
78
+ )
79
+ : json({ exc_type: "ValidationError", exception: "Move Usage timed out" }, 417);
80
+ }
81
+ if (method === "frappe.auth.get_logged_user" || method.endsWith(".get_roles")) {
82
+ return json({ exc_type: "PermissionError" }, 403);
83
+ }
84
+ const resource = `/api/resource/${encodeURIComponent(doctype)}`;
85
+ if (url.pathname === resource || url.pathname.startsWith(`${resource}/`)) {
86
+ if (!recordsReadable) {
87
+ return json({ exc_type: "PermissionError", exception: "Not permitted" }, 403);
88
+ }
89
+ return url.pathname === resource ? json({ data: [tackle] }) : json({ data: tackle });
90
+ }
91
+ return json({ data: [], message: [] });
92
+ });
93
+ }
94
+
95
+ function renderAt(pathname: string, page: "list" | "form") {
96
+ const navigation: FrappeNavigation = {
97
+ usePathname: () => pathname,
98
+ useNavigate: () => () => undefined,
99
+ };
100
+ return renderWithProviders(
101
+ <FrappeProvider baseURL={bench} realtime={false} navigation={navigation}>
102
+ {page === "list" ? (
103
+ <FrappeListView<Move> doctype={doctype} route={route} defaultViewMode="table" />
104
+ ) : (
105
+ <FrappeForm<Move> doctype={doctype} route={route} />
106
+ )}
107
+ </FrappeProvider>,
108
+ );
109
+ }
110
+
111
+ beforeAll(async () => {
112
+ await i18next.use(initReactI18next).init({
113
+ lng: "en",
114
+ interpolation: { escapeValue: false },
115
+ resources: { en: { translation: {} } },
116
+ });
117
+ });
118
+
119
+ beforeEach(() => {
120
+ run += 1;
121
+ bench = `http://guest-fallback-${run}.spec.test`;
122
+ metaAnswer = "refused";
123
+ recordsReadable = true;
124
+ reportRun = "refused";
125
+ stubBench();
126
+ });
127
+
128
+ afterEach(() => {
129
+ cleanup();
130
+ vi.unstubAllGlobals();
131
+ });
132
+
133
+ describe("LC-96 guest fallback: meta refused, rows readable", () => {
134
+ it("a record page lays out the record's own fields, read-only", async () => {
135
+ expect(frappeRouteTarget(route, "/moves/a1b2c3")).toEqual({ mode: "view", name: "a1b2c3" });
136
+ renderAt("/moves/a1b2c3", "form");
137
+ const power = (await screen.findByLabelText("Power", { exact: false }, settle)) as HTMLElement;
138
+ const input = power instanceof HTMLInputElement ? power : power.querySelector("input");
139
+ expect(input?.value).toBe("40");
140
+ expect(input?.disabled || input?.readOnly).toBe(true);
141
+ expect(screen.getByLabelText("Accuracy", { exact: false })).toBeTruthy();
142
+ for (const bookkeeping of ["Owner", "Modified", "Docstatus", "Idx", "User Tags"]) {
143
+ expect(screen.queryByLabelText(bookkeeping, { exact: true })).toBeNull();
144
+ }
145
+ });
146
+
147
+ it("a list table takes its columns from one record instead of a lone Name", async () => {
148
+ renderAt("/moves", "list");
149
+ await waitFor(() => expect(screen.getAllByText("tackle").length).toBeGreaterThan(0), settle);
150
+ await waitFor(() => expect(screen.getByText("Move Name")).toBeTruthy(), settle);
151
+ expect(screen.getByText("Power")).toBeTruthy();
152
+ expect(screen.queryByText("Owner")).toBeNull();
153
+ });
154
+ });
155
+
156
+ describe("LC-96 guest fallback: nothing readable to fall back to", () => {
157
+ it("a create page says the reader can't create, with the server's reason, not a load failure", async () => {
158
+ renderAt("/moves/new", "form");
159
+ const state = await screen.findByTestId("frappe-form-not-permitted", {}, settle);
160
+ expect(state.textContent).toContain(`You can't create ${doctype}`);
161
+ expect(screen.queryByText(`Failed to load ${doctype} form`)).toBeNull();
162
+ });
163
+
164
+ it("a record page whose record is refused too says the reader can't open it, with no retry", async () => {
165
+ recordsReadable = false;
166
+ renderAt("/moves/a1b2c3", "form");
167
+ const state = await screen.findByTestId("frappe-form-not-permitted", {}, settle);
168
+ expect(state.textContent).toContain(`You can't open ${doctype} a1b2c3`);
169
+ expect(screen.queryByText(`Failed to load ${doctype}`)).toBeNull();
170
+ expect(screen.queryByRole("button", { name: /retry/i })).toBeNull();
171
+ });
172
+
173
+ it("a meta read that failed for another reason is an error state, not a permission one", async () => {
174
+ metaAnswer = "broken";
175
+ renderAt("/moves/new", "form");
176
+ const alert = await screen.findByText(`Failed to load ${doctype} form`, {}, settle);
177
+ expect(alert).toBeTruthy();
178
+ expect(screen.queryByTestId("frappe-form-not-permitted")).toBeNull();
179
+ });
180
+ });
181
+
182
+ function renderReport(view: "builder" | "query") {
183
+ return renderWithProviders(
184
+ <FrappeProvider baseURL={bench} realtime={false}>
185
+ {view === "builder" ? (
186
+ <FrappeReportBuilder doctype={doctype} />
187
+ ) : (
188
+ <FrappeReportView reportName={reportName} debounceMs={0} />
189
+ )}
190
+ </FrappeProvider>,
191
+ );
192
+ }
193
+
194
+ describe("LC-96 guest fallback: report views", () => {
195
+ it("a doctype report takes its columns from one record and saves nothing", async () => {
196
+ renderReport("builder");
197
+ await waitFor(() => expect(screen.getAllByText("tackle").length).toBeGreaterThan(0), settle);
198
+ expect(screen.getByText("Move Name")).toBeTruthy();
199
+ expect(screen.getByText("Power")).toBeTruthy();
200
+ expect(screen.getByText("Accuracy")).toBeTruthy();
201
+ expect(screen.queryByText("Owner")).toBeNull();
202
+ expect(screen.queryByText(/Couldn't load/)).toBeNull();
203
+ expect(screen.queryByRole("button", { name: /save as/i })).toBeNull();
204
+ });
205
+
206
+ it("a doctype report whose rows are refused too says the reader can't open it, with no retry", async () => {
207
+ recordsReadable = false;
208
+ renderReport("builder");
209
+ const state = await screen.findByTestId("frappe-report-not-permitted", {}, settle);
210
+ expect(state.textContent).toContain(`You can't open the ${doctype} report`);
211
+ expect(screen.queryByRole("button", { name: /retry/i })).toBeNull();
212
+ });
213
+
214
+ it("a doctype report whose meta failed for another reason stays an error", async () => {
215
+ metaAnswer = "broken";
216
+ renderReport("builder");
217
+ expect(await screen.findByText(`Couldn't load the ${doctype} report`, {}, settle)).toBeTruthy();
218
+ expect(screen.queryByTestId("frappe-report-not-permitted")).toBeNull();
219
+ });
220
+
221
+ it("a refused query report says the reader can't run it, with the server's reason and no retry", async () => {
222
+ renderReport("query");
223
+ const state = await screen.findByTestId("frappe-report-not-permitted", {}, settle);
224
+ expect(state.textContent).toContain(`You can't run ${reportName}`);
225
+ expect(state.textContent).toContain(`You don't have access to Report: ${reportName}`);
226
+ expect(screen.queryByRole("button", { name: /retry|refresh/i })).toBeNull();
227
+ });
228
+
229
+ it("a query report that failed for another reason stays a retryable error", async () => {
230
+ reportRun = "broken";
231
+ renderReport("query");
232
+ expect(await screen.findByText(`Error running ${reportName}`, {}, settle)).toBeTruthy();
233
+ expect(screen.queryByTestId("frappe-report-not-permitted")).toBeNull();
234
+ expect(screen.getByRole("button", { name: /retry/i })).toBeTruthy();
235
+ });
236
+ });
package/src/index.ts CHANGED
@@ -54,6 +54,15 @@ export {
54
54
 
55
55
  export { FrappeTable, filterValueToFrappeTuple, type FrappeTableProps } from "./FrappeTable";
56
56
  export { FrappeForm, type FrappeConnectionRows, type FrappeFormProps } from "./FrappeForm";
57
+ export {
58
+ useFrappeForm,
59
+ type FrappeFormConnectionConfig,
60
+ type FrappeFormController,
61
+ type FrappeFormFieldTarget,
62
+ type FrappeFormMetaSource,
63
+ type FrappeFormStatus,
64
+ type UseFrappeFormOptions,
65
+ } from "./useFrappeForm";
57
66
  export {
58
67
  FrappeFormConnectionSection,
59
68
  FrappeFormConnections,
@@ -146,13 +155,6 @@ export { FrappeGantt, type FrappeGanttProps } from "./FrappeGantt";
146
155
 
147
156
  export { resolveFrappeEventColor, frappeEventColorNames } from "./frappeEventColor";
148
157
 
149
- export {
150
- FrappeReportBuilder,
151
- type FrappeReportBuilderProps,
152
- type ReportColumn,
153
- type GroupByConfig,
154
- } from "./FrappeReportBuilder";
155
-
156
158
  export {
157
159
  FrappeDashboard,
158
160
  type FrappeDashboardProps,
@@ -207,6 +209,17 @@ export {
207
209
  type UseFrappeRowsResult,
208
210
  } from "./useFrappeRows";
209
211
 
212
+ // What a view lays itself out from when meta is refused (LC-96).
213
+ export {
214
+ fieldsFromRecord,
215
+ recordFieldKeys,
216
+ recordKeyLabel,
217
+ useRecordShape,
218
+ type RecordShape,
219
+ type RecordShapeConnection,
220
+ } from "./recordShape";
221
+ export { isPermissionError, refusalReason } from "./liveErrors";
222
+
210
223
  export {
211
224
  useFrappeAggregate,
212
225
  type FrappeAggregateConnection,
@@ -455,32 +468,118 @@ export { useFrappeCollection, useLiveQuery } from "@multiplatform.one/frappe";
455
468
  export type { DataTableColumn } from "@multiplatform.one/table";
456
469
 
457
470
  // ---------------------------------------------------------------------------
458
- // Report View (server-side Query / Script reports)
471
+ // Report models (MPO-15, GL-28): a named query/script report and a doctype's
472
+ // report view, one model each. FrappeReportView and the desk's
473
+ // QueryReportScreen render the first through one transport;
474
+ // FrappeReportBuilder, the desk's view=report, renders the second.
459
475
  // ---------------------------------------------------------------------------
460
476
 
461
477
  export {
462
478
  FrappeReportView,
463
- useFrappeReport,
479
+ reportColumnToDataTableColumn,
480
+ reportFieldtypeToTableField,
481
+ reportRowLabel,
482
+ type FrappeReportViewProps,
483
+ } from "./FrappeReportView";
484
+ export {
485
+ buildRunFilters,
464
486
  compactFilterValues,
487
+ compactQueryReportFilters,
488
+ defaultFilterValues,
489
+ extractScriptFilters,
490
+ humanizeFrappeError,
491
+ isEmptyFilterValue,
492
+ normalizeFilterMeta,
493
+ normalizeQueryReportColumns,
494
+ normalizeQueryReportPayload,
495
+ normalizeQueryReportRows,
465
496
  normalizeReportColumn,
466
497
  normalizeReportPayload,
467
498
  normalizeReportRows,
468
- reportColumnToDataTableColumn,
469
- reportFieldtypeToTableField,
470
- reportRowLabel,
471
499
  scrubFieldname,
500
+ stripHtml as stripReportHtml,
472
501
  type FrappeReportColumn,
473
- type FrappeReportConnection,
474
502
  type FrappeReportFilterDef,
475
- type FrappeReportFixture,
476
503
  type FrappeReportPayload,
477
504
  type FrappeReportRow,
478
505
  type FrappeReportRunResponse,
479
506
  type FrappeReportValues,
480
- type FrappeReportViewProps,
507
+ type QueryReportColumn,
508
+ type QueryReportFilterInput,
509
+ type QueryReportFilterMeta,
510
+ type QueryReportFilters,
511
+ type QueryReportFixture,
512
+ type QueryReportMeta,
513
+ type QueryReportPayload,
514
+ type QueryReportRow,
515
+ type QueryReportRunMessage,
516
+ type ScriptFilterExtraction,
517
+ } from "./report/queryReport";
518
+ export {
519
+ reportCall,
520
+ useFrappeReport,
521
+ type FrappeReportConnection,
522
+ type FrappeReportFixture,
481
523
  type UseFrappeReportOptions,
482
524
  type UseFrappeReportResult,
483
- } from "./FrappeReportView";
525
+ } from "./report/useFrappeReport";
526
+ export {
527
+ useQueryReport,
528
+ type UseQueryReportOptions,
529
+ type UseQueryReportResult,
530
+ } from "./report/useQueryReport";
531
+ export {
532
+ buildQueryReportColumns,
533
+ buildQueryReportCsv,
534
+ type BuildQueryReportColumnsArgs,
535
+ } from "./report/queryReportColumns";
536
+ export {
537
+ FrappeReportBuilder,
538
+ type FrappeReportBuilderProps,
539
+ type ReportColumn,
540
+ type GroupByConfig,
541
+ } from "./FrappeReportBuilder";
542
+ export {
543
+ buildGridRows as buildReportGridRows,
544
+ buildReportCsv,
545
+ columnAlign as reportColumnAlign,
546
+ columnFilterTuple as reportColumnFilterTuple,
547
+ columnFilterTuples as reportColumnFilterTuples,
548
+ compareCellValues as compareReportCellValues,
549
+ computeTotals as computeReportTotals,
550
+ DEFAULT_REPORT_PAGE_SIZE,
551
+ defaultColumnWidth as defaultReportColumnWidth,
552
+ defaultReportFieldnames,
553
+ fromDeskReportSettings,
554
+ orderReportRows,
555
+ parseDeskOrderBy,
556
+ parseReportFiltersParam,
557
+ REPORT_NUMERIC_FIELDTYPES,
558
+ REPORT_PAGE_SIZES,
559
+ reportColumnsFromMeta,
560
+ resolveSelectedColumns as resolveReportColumns,
561
+ toDeskReportSettings,
562
+ type DeskReportSettings,
563
+ type ReportColumnDef,
564
+ type ReportDataRow,
565
+ type ReportFilters,
566
+ type ReportGridRow,
567
+ type ReportGroupHeader,
568
+ type ReportMetaSource,
569
+ type ReportRow,
570
+ type ReportSortState,
571
+ type ReportTotalsCell,
572
+ type ReportViewConfig,
573
+ } from "./report/reportViewModel";
574
+ export { ReportGrid, type ReportGridProps } from "./report/ReportGrid";
575
+ export { ReportViewToolbar, type ReportViewToolbarProps } from "./report/ReportViewToolbar";
576
+ export {
577
+ useSavedReports,
578
+ type SavedReportSummary,
579
+ type SavedReportsConnection,
580
+ type UseSavedReportsResult,
581
+ } from "./report/savedReports";
582
+ export { dialogBodyMinWidth, overlayListMaxHeight } from "./measures";
484
583
 
485
584
  // ---------------------------------------------------------------------------
486
585
  // Desk Form Engine (MPO-15)
@@ -507,7 +606,9 @@ export {
507
606
  canReadLevel,
508
607
  canWriteLevel,
509
608
  computeDirtyFields,
609
+ docinfoRights,
510
610
  docRightNames,
611
+ fixtureDocRights,
511
612
  isEmptyValue,
512
613
  isFetchLocked,
513
614
  isPromptNamed,
@@ -8,6 +8,16 @@ describe("isPermissionError", () => {
8
8
  expect(isPermissionError(Object.assign(new Error("nope"), { status: 500 }))).toBe(false);
9
9
  });
10
10
 
11
+ it("treats a PermissionError exc_type as permission, whatever the sentence", () => {
12
+ const err = Object.assign(new Error("You don't have access to Report: ToDo"), {
13
+ exc_type: "PermissionError",
14
+ });
15
+ expect(isPermissionError(err)).toBe(true);
16
+ expect(
17
+ isPermissionError(Object.assign(new Error("Timed out"), { exc_type: "ValidationError" })),
18
+ ).toBe(false);
19
+ });
20
+
11
21
  it("treats a PermissionError-shaped message as permission", () => {
12
22
  expect(isPermissionError(new Error("PermissionError"))).toBe(true);
13
23
  expect(isPermissionError(new Error("You don't have access to Report: ToDo"))).toBe(false);
package/src/liveErrors.ts CHANGED
@@ -9,10 +9,14 @@
9
9
  * from "the load failed".
10
10
  */
11
11
 
12
- /** True when the error is an auth/permission rejection (403 or PermissionError). */
12
+ /**
13
+ * True when the error is an auth/permission rejection: a 403 or 401, a
14
+ * PermissionError `exc_type`, or a PermissionError-shaped message.
15
+ */
13
16
  export function isPermissionError(error: unknown): boolean {
14
- const status = (error as { status?: number } | undefined)?.status;
15
- if (status === 403 || status === 401) return true;
17
+ const shaped = error as { status?: number; exc_type?: unknown } | undefined;
18
+ if (shaped?.status === 403 || shaped?.status === 401) return true;
19
+ if (shaped?.exc_type === "PermissionError") return true;
16
20
  const message = error instanceof Error ? error.message : String(error ?? "");
17
21
  return /permission|not permitted|forbidden/i.test(message);
18
22
  }
@@ -22,6 +26,15 @@ function isTracebackDetail(detail: string): boolean {
22
26
  return /Traceback \(most recent call last\)/i.test(detail);
23
27
  }
24
28
 
29
+ /**
30
+ * The server's own sentence for a refusal, for a permission state's
31
+ * description: undefined when there is none or it is a python traceback.
32
+ */
33
+ export function refusalReason(error: unknown): string | undefined {
34
+ const detail = error instanceof Error ? error.message : String(error ?? "");
35
+ return detail && !isTracebackDetail(detail) ? detail : undefined;
36
+ }
37
+
25
38
  /**
26
39
  * Normalizes an initial-sync failure into an Error with an honest,
27
40
  * user-facing message. `permissionMessage` / `genericMessage` come from the
@@ -0,0 +1,11 @@
1
+ import { sizeRecipeEscape } from "@multiplatform.one/theme";
2
+
3
+ export const dialogBodyMinWidth = sizeRecipeEscape(
4
+ 280,
5
+ "dialog body floor: a reading width for the form inside a dialog or popover",
6
+ );
7
+
8
+ export const overlayListMaxHeight = sizeRecipeEscape(
9
+ 360,
10
+ "overlay list cap: a menu or popover list scrolls past this height instead of growing",
11
+ );