@multiplatform.one/frappe-ui 7.88.0 → 7.88.2

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 (330) hide show
  1. package/dist/cjs/ChartCard.native.js.map +1 -1
  2. package/dist/cjs/FileBrowser/AttachLibrary.native.js.map +1 -1
  3. package/dist/cjs/FileBrowser/folderTree.native.js.map +1 -1
  4. package/dist/cjs/FileBrowser/index.native.js.map +1 -1
  5. package/dist/cjs/FileBrowser/types.native.js.map +1 -1
  6. package/dist/cjs/FileBrowser/viewPersistence.native.js.map +1 -1
  7. package/dist/cjs/FormSidebar.native.js.map +1 -1
  8. package/dist/cjs/FrappeAssignment.native.js.map +1 -1
  9. package/dist/cjs/FrappeCalendar.native.js.map +1 -1
  10. package/dist/cjs/FrappeChartCard.cjs +5 -3
  11. package/dist/cjs/FrappeChartCard.native.js +5 -3
  12. package/dist/cjs/FrappeChartCard.native.js.map +1 -1
  13. package/dist/cjs/FrappeConnectionIndicator.native.js.map +1 -1
  14. package/dist/cjs/FrappeDashboard.native.js.map +1 -1
  15. package/dist/cjs/FrappeForm.cjs +6 -2
  16. package/dist/cjs/FrappeForm.native.js +7 -3
  17. package/dist/cjs/FrappeForm.native.js.map +1 -1
  18. package/dist/cjs/FrappeFormActions.native.js.map +1 -1
  19. package/dist/cjs/FrappeFormConnections.cjs +3 -3
  20. package/dist/cjs/FrappeFormConnections.native.js +3 -3
  21. package/dist/cjs/FrappeFormConnections.native.js.map +1 -1
  22. package/dist/cjs/FrappeGantt.native.js.map +1 -1
  23. package/dist/cjs/FrappeImageView.native.js.map +1 -1
  24. package/dist/cjs/FrappeInsights.native.js.map +1 -1
  25. package/dist/cjs/FrappeKPICard.cjs +5 -3
  26. package/dist/cjs/FrappeKPICard.native.js +5 -3
  27. package/dist/cjs/FrappeKPICard.native.js.map +1 -1
  28. package/dist/cjs/FrappeKanban.native.js.map +1 -1
  29. package/dist/cjs/FrappeLikedBy.native.js.map +1 -1
  30. package/dist/cjs/FrappeList.cjs +3 -3
  31. package/dist/cjs/FrappeList.native.js +3 -3
  32. package/dist/cjs/FrappeList.native.js.map +1 -1
  33. package/dist/cjs/FrappeListView.native.js.map +1 -1
  34. package/dist/cjs/FrappeNotConfigured.native.js.map +1 -1
  35. package/dist/cjs/FrappeProvider.native.js.map +1 -1
  36. package/dist/cjs/FrappeReconnectIndicator.native.js.map +1 -1
  37. package/dist/cjs/FrappeRecordView.native.js.map +1 -1
  38. package/dist/cjs/FrappeReportBuilder.cjs +2 -1
  39. package/dist/cjs/FrappeReportBuilder.native.js +2 -1
  40. package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
  41. package/dist/cjs/FrappeReportView.native.js +3 -3
  42. package/dist/cjs/FrappeReportView.native.js.map +1 -1
  43. package/dist/cjs/FrappeRowsReader.native.js.map +1 -1
  44. package/dist/cjs/FrappeTable.cjs +19 -3
  45. package/dist/cjs/FrappeTable.native.js +19 -3
  46. package/dist/cjs/FrappeTable.native.js.map +1 -1
  47. package/dist/cjs/FrappeTreeView.native.js.map +1 -1
  48. package/dist/cjs/SyncErrorNotice.native.js.map +1 -1
  49. package/dist/cjs/commentActions.native.js.map +1 -1
  50. package/dist/cjs/components/attachments.native.js.map +1 -1
  51. package/dist/cjs/components/tags.native.js.map +1 -1
  52. package/dist/cjs/components/timeline.native.js.map +1 -1
  53. package/dist/cjs/dependsOn.native.js.map +1 -1
  54. package/dist/cjs/desk/DeskFormPage.native.js.map +1 -1
  55. package/dist/cjs/desk/DeskFormSidebar.native.js.map +1 -1
  56. package/dist/cjs/desk/DeskListPage.native.js.map +1 -1
  57. package/dist/cjs/desk/DeskShell.native.js.map +1 -1
  58. package/dist/cjs/desk/DeskSidebar.native.js.map +1 -1
  59. package/dist/cjs/desk/awesomebar.native.js.map +1 -1
  60. package/dist/cjs/fieldOverrides.native.js.map +1 -1
  61. package/dist/cjs/fieldRegistry.native.js +31 -31
  62. package/dist/cjs/fieldRegistry.native.js.map +1 -1
  63. package/dist/cjs/fields/Barcode/index.native.js.map +1 -1
  64. package/dist/cjs/fields/Check/index.native.js.map +1 -1
  65. package/dist/cjs/fields/CodeEditor/index.native.js.map +1 -1
  66. package/dist/cjs/fields/Color/index.native.js.map +1 -1
  67. package/dist/cjs/fields/Data/index.native.js.map +1 -1
  68. package/dist/cjs/fields/Date/catalog.native.js +1 -1
  69. package/dist/cjs/fields/Date/catalog.native.js.map +1 -1
  70. package/dist/cjs/fields/Date/index.native.js.map +1 -1
  71. package/dist/cjs/fields/Date/types.native.js.map +1 -1
  72. package/dist/cjs/fields/Date/wire.native.js.map +1 -1
  73. package/dist/cjs/fields/DateRange/index.native.js.map +1 -1
  74. package/dist/cjs/fields/Duration/index.native.js.map +1 -1
  75. package/dist/cjs/fields/DynamicLink.native.js.map +1 -1
  76. package/dist/cjs/fields/FileUpload.native.js.map +1 -1
  77. package/dist/cjs/fields/FileUploadField/index.native.js.map +1 -1
  78. package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
  79. package/dist/cjs/fields/HTML/index.native.js.map +1 -1
  80. package/dist/cjs/fields/Heading/index.native.js.map +1 -1
  81. package/dist/cjs/fields/Icon/index.native.js.map +1 -1
  82. package/dist/cjs/fields/Link.native.js.map +1 -1
  83. package/dist/cjs/fields/LinkTree.native.js.map +1 -1
  84. package/dist/cjs/fields/LongText/index.native.js.map +1 -1
  85. package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
  86. package/dist/cjs/fields/Numeric/index.native.js.map +1 -1
  87. package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
  88. package/dist/cjs/fields/Password/index.native.js.map +1 -1
  89. package/dist/cjs/fields/Phone/index.native.js.map +1 -1
  90. package/dist/cjs/fields/Progress/index.native.js.map +1 -1
  91. package/dist/cjs/fields/RadioGroup/index.native.js.map +1 -1
  92. package/dist/cjs/fields/Rating/index.native.js.map +1 -1
  93. package/dist/cjs/fields/ReadOnly/index.native.js.map +1 -1
  94. package/dist/cjs/fields/Select/index.native.js.map +1 -1
  95. package/dist/cjs/fields/Signature/index.native.js.map +1 -1
  96. package/dist/cjs/fields/Slider/index.native.js.map +1 -1
  97. package/dist/cjs/fields/SmallText/index.native.js.map +1 -1
  98. package/dist/cjs/fields/Switch/index.native.js.map +1 -1
  99. package/dist/cjs/fields/TableMultiSelect.native.js.map +1 -1
  100. package/dist/cjs/fields/TextEditor/types.native.js.map +1 -1
  101. package/dist/cjs/fields/TimePicker/index.native.js.map +1 -1
  102. package/dist/cjs/fields/ToggleGroup/index.native.js.map +1 -1
  103. package/dist/cjs/fields/shared/EditorPreviewPanes.native.js.map +1 -1
  104. package/dist/cjs/filterFields.native.js.map +1 -1
  105. package/dist/cjs/formEngine/createPrefill.native.js.map +1 -1
  106. package/dist/cjs/formEngine/dirtyTracking.native.js.map +1 -1
  107. package/dist/cjs/formEngine/docLifecycle.native.js.map +1 -1
  108. package/dist/cjs/formEngine/docPermissions.native.js.map +1 -1
  109. package/dist/cjs/formEngine/doctypeMeta.native.js.map +1 -1
  110. package/dist/cjs/formEngine/fetchFrom.native.js.map +1 -1
  111. package/dist/cjs/formEngine/fieldPermissions.native.js.map +1 -1
  112. package/dist/cjs/formEngine/formActions.native.js.map +1 -1
  113. package/dist/cjs/formEngine/formLayout.native.js.map +1 -1
  114. package/dist/cjs/formEngine/index.native.js.map +1 -1
  115. package/dist/cjs/formEngine/useAmendedAs.native.js.map +1 -1
  116. package/dist/cjs/formEngine/useDocRights.native.js.map +1 -1
  117. package/dist/cjs/formEngine/useFormEngine.native.js.map +1 -1
  118. package/dist/cjs/formEngine/useSessionRoles.native.js.map +1 -1
  119. package/dist/cjs/frappeConnections.native.js.map +1 -1
  120. package/dist/cjs/frappeEventColor.native.js.map +1 -1
  121. package/dist/cjs/frappeNavigation.native.js.map +1 -1
  122. package/dist/cjs/frappeStoryTarget.native.js.map +1 -1
  123. package/dist/cjs/frappeUsers.native.js.map +1 -1
  124. package/dist/cjs/index.native.js +34 -34
  125. package/dist/cjs/index.native.js.map +1 -1
  126. package/dist/cjs/insights/InsightsFacetTags.native.js.map +1 -1
  127. package/dist/cjs/insights/InsightsKpiBand.native.js.map +1 -1
  128. package/dist/cjs/insights/InsightsLeaders.native.js.map +1 -1
  129. package/dist/cjs/insights/InsightsRecords.native.js.map +1 -1
  130. package/dist/cjs/insights/InsightsTable.native.js.map +1 -1
  131. package/dist/cjs/insights/insightsData.native.js.map +1 -1
  132. package/dist/cjs/insights/insightsRecord.native.js.map +1 -1
  133. package/dist/cjs/list/BandFilters.native.js.map +1 -1
  134. package/dist/cjs/list/FacetBadge.native.js.map +1 -1
  135. package/dist/cjs/list/FacetList.native.js.map +1 -1
  136. package/dist/cjs/list/FacetRail.native.js.map +1 -1
  137. package/dist/cjs/list/ImageWell.native.js.map +1 -1
  138. package/dist/cjs/list/ListControls.native.js.map +1 -1
  139. package/dist/cjs/list/ListControlsSheet.native.js.map +1 -1
  140. package/dist/cjs/list/ListFilterPanel.native.js.map +1 -1
  141. package/dist/cjs/list/ListFiltersButton.native.js.map +1 -1
  142. package/dist/cjs/list/ListSidebarToolbar.native.js.map +1 -1
  143. package/dist/cjs/list/ListToastHost.native.js.map +1 -1
  144. package/dist/cjs/list/ListToolsSheet.native.js.map +1 -1
  145. package/dist/cjs/list/RangeFilter.native.js.map +1 -1
  146. package/dist/cjs/list/ShufflePanel.native.js.map +1 -1
  147. package/dist/cjs/list/defaultProjection.native.js.map +1 -1
  148. package/dist/cjs/list/facetBadgeSteps.native.js.map +1 -1
  149. package/dist/cjs/list/facetReading.native.js.map +1 -1
  150. package/dist/cjs/list/favorites.native.js.map +1 -1
  151. package/dist/cjs/list/listColumns.native.js.map +1 -1
  152. package/dist/cjs/list/listControls.fixture.native.js.map +1 -1
  153. package/dist/cjs/list/listFields.native.js.map +1 -1
  154. package/dist/cjs/list/listFormat.native.js.map +1 -1
  155. package/dist/cjs/list/listState.native.js.map +1 -1
  156. package/dist/cjs/list/recordLook.native.js.map +1 -1
  157. package/dist/cjs/list/statusColors.native.js.map +1 -1
  158. package/dist/cjs/list/useListControls.native.js.map +1 -1
  159. package/dist/cjs/liveErrors.cjs +15 -0
  160. package/dist/cjs/liveErrors.native.js +15 -0
  161. package/dist/cjs/liveErrors.native.js.map +1 -1
  162. package/dist/cjs/measures.native.js.map +1 -1
  163. package/dist/cjs/record/RecordBars.native.js.map +1 -1
  164. package/dist/cjs/record/RecordGallery.native.js.map +1 -1
  165. package/dist/cjs/record/RecordHero.native.js.map +1 -1
  166. package/dist/cjs/record/RecordPage.native.js.map +1 -1
  167. package/dist/cjs/record/RecordPager.native.js.map +1 -1
  168. package/dist/cjs/record/RecordRows.native.js.map +1 -1
  169. package/dist/cjs/record/recordData.native.js.map +1 -1
  170. package/dist/cjs/record/recordSections.native.js.map +1 -1
  171. package/dist/cjs/recordShape.native.js.map +1 -1
  172. package/dist/cjs/report/ReportGrid.cjs +18 -4
  173. package/dist/cjs/report/ReportGrid.native.js +28 -4
  174. package/dist/cjs/report/ReportGrid.native.js.map +1 -1
  175. package/dist/cjs/report/ReportViewToolbar.cjs +8 -13
  176. package/dist/cjs/report/ReportViewToolbar.native.js +8 -13
  177. package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -1
  178. package/dist/cjs/report/exportCsv.native.js.map +1 -1
  179. package/dist/cjs/report/queryReport.native.js.map +1 -1
  180. package/dist/cjs/report/queryReportColumns.native.js.map +1 -1
  181. package/dist/cjs/report/reportViewModel.native.js.map +1 -1
  182. package/dist/cjs/report/savedReports.native.js.map +1 -1
  183. package/dist/cjs/report/useFrappeReport.native.js.map +1 -1
  184. package/dist/cjs/report/useQueryReport.native.js.map +1 -1
  185. package/dist/cjs/resolveControl.native.js.map +1 -1
  186. package/dist/cjs/schemaForm/SchemaWizard.native.js.map +1 -1
  187. package/dist/cjs/schemaForm/applyFieldOrder.native.js.map +1 -1
  188. package/dist/cjs/schemaForm/index.cjs +12 -2
  189. package/dist/cjs/schemaForm/index.native.js +17 -3
  190. package/dist/cjs/schemaForm/index.native.js.map +1 -1
  191. package/dist/cjs/sessionUser.native.js.map +1 -1
  192. package/dist/cjs/shared/devWarn.native.js.map +1 -1
  193. package/dist/cjs/shared/useKeyboardInset.native.js.map +1 -1
  194. package/dist/cjs/syncError.native.js.map +1 -1
  195. package/dist/cjs/useConnectionFilters.native.js.map +1 -1
  196. package/dist/cjs/useFrappeAggregate.native.js.map +1 -1
  197. package/dist/cjs/useFrappeFacets.native.js.map +1 -1
  198. package/dist/cjs/useFrappeForm.native.js.map +1 -1
  199. package/dist/cjs/useFrappePagination.native.js.map +1 -1
  200. package/dist/cjs/useFrappeRows.native.js.map +1 -1
  201. package/dist/esm/FrappeChartCard.mjs +5 -3
  202. package/dist/esm/FrappeChartCard.mjs.map +1 -1
  203. package/dist/esm/FrappeChartCard.native.js +5 -3
  204. package/dist/esm/FrappeChartCard.native.js.map +1 -1
  205. package/dist/esm/FrappeForm.mjs +7 -3
  206. package/dist/esm/FrappeForm.mjs.map +1 -1
  207. package/dist/esm/FrappeForm.native.js +7 -3
  208. package/dist/esm/FrappeForm.native.js.map +1 -1
  209. package/dist/esm/FrappeFormConnections.mjs +4 -4
  210. package/dist/esm/FrappeFormConnections.mjs.map +1 -1
  211. package/dist/esm/FrappeFormConnections.native.js +4 -4
  212. package/dist/esm/FrappeFormConnections.native.js.map +1 -1
  213. package/dist/esm/FrappeKPICard.mjs +5 -3
  214. package/dist/esm/FrappeKPICard.mjs.map +1 -1
  215. package/dist/esm/FrappeKPICard.native.js +5 -3
  216. package/dist/esm/FrappeKPICard.native.js.map +1 -1
  217. package/dist/esm/FrappeList.mjs +4 -4
  218. package/dist/esm/FrappeList.mjs.map +1 -1
  219. package/dist/esm/FrappeList.native.js +4 -4
  220. package/dist/esm/FrappeList.native.js.map +1 -1
  221. package/dist/esm/FrappeReportBuilder.mjs +2 -1
  222. package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
  223. package/dist/esm/FrappeReportBuilder.native.js +2 -1
  224. package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
  225. package/dist/esm/FrappeTable.mjs +20 -4
  226. package/dist/esm/FrappeTable.mjs.map +1 -1
  227. package/dist/esm/FrappeTable.native.js +20 -4
  228. package/dist/esm/FrappeTable.native.js.map +1 -1
  229. package/dist/esm/liveErrors.mjs +15 -1
  230. package/dist/esm/liveErrors.mjs.map +1 -1
  231. package/dist/esm/liveErrors.native.js +15 -1
  232. package/dist/esm/liveErrors.native.js.map +1 -1
  233. package/dist/esm/report/ReportGrid.mjs +17 -4
  234. package/dist/esm/report/ReportGrid.mjs.map +1 -1
  235. package/dist/esm/report/ReportGrid.native.js +27 -4
  236. package/dist/esm/report/ReportGrid.native.js.map +1 -1
  237. package/dist/esm/report/ReportViewToolbar.mjs +8 -13
  238. package/dist/esm/report/ReportViewToolbar.mjs.map +1 -1
  239. package/dist/esm/report/ReportViewToolbar.native.js +8 -13
  240. package/dist/esm/report/ReportViewToolbar.native.js.map +1 -1
  241. package/dist/esm/schemaForm/index.mjs +12 -3
  242. package/dist/esm/schemaForm/index.mjs.map +1 -1
  243. package/dist/esm/schemaForm/index.native.js +16 -3
  244. package/dist/esm/schemaForm/index.native.js.map +1 -1
  245. package/dist/jsx/FrappeChartCard.mjs +5 -3
  246. package/dist/jsx/FrappeChartCard.mjs.map +1 -1
  247. package/dist/jsx/FrappeChartCard.native.js +5 -3
  248. package/dist/jsx/FrappeChartCard.native.js.map +1 -1
  249. package/dist/jsx/FrappeForm.mjs +7 -3
  250. package/dist/jsx/FrappeForm.mjs.map +1 -1
  251. package/dist/jsx/FrappeForm.native.js +6 -2
  252. package/dist/jsx/FrappeForm.native.js.map +1 -1
  253. package/dist/jsx/FrappeFormConnections.mjs +4 -4
  254. package/dist/jsx/FrappeFormConnections.mjs.map +1 -1
  255. package/dist/jsx/FrappeFormConnections.native.js +3 -3
  256. package/dist/jsx/FrappeFormConnections.native.js.map +1 -1
  257. package/dist/jsx/FrappeKPICard.mjs +5 -3
  258. package/dist/jsx/FrappeKPICard.mjs.map +1 -1
  259. package/dist/jsx/FrappeKPICard.native.js +5 -3
  260. package/dist/jsx/FrappeKPICard.native.js.map +1 -1
  261. package/dist/jsx/FrappeList.mjs +4 -4
  262. package/dist/jsx/FrappeList.mjs.map +1 -1
  263. package/dist/jsx/FrappeList.native.js +3 -3
  264. package/dist/jsx/FrappeList.native.js.map +1 -1
  265. package/dist/jsx/FrappeReportBuilder.mjs +2 -1
  266. package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
  267. package/dist/jsx/FrappeReportBuilder.native.js +2 -1
  268. package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
  269. package/dist/jsx/FrappeTable.mjs +20 -4
  270. package/dist/jsx/FrappeTable.mjs.map +1 -1
  271. package/dist/jsx/FrappeTable.native.js +19 -3
  272. package/dist/jsx/FrappeTable.native.js.map +1 -1
  273. package/dist/jsx/liveErrors.mjs +15 -1
  274. package/dist/jsx/liveErrors.mjs.map +1 -1
  275. package/dist/jsx/liveErrors.native.js +15 -0
  276. package/dist/jsx/liveErrors.native.js.map +1 -1
  277. package/dist/jsx/report/ReportGrid.mjs +17 -4
  278. package/dist/jsx/report/ReportGrid.mjs.map +1 -1
  279. package/dist/jsx/report/ReportGrid.native.js +28 -4
  280. package/dist/jsx/report/ReportGrid.native.js.map +1 -1
  281. package/dist/jsx/report/ReportViewToolbar.mjs +8 -13
  282. package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -1
  283. package/dist/jsx/report/ReportViewToolbar.native.js +8 -13
  284. package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -1
  285. package/dist/jsx/schemaForm/index.mjs +12 -3
  286. package/dist/jsx/schemaForm/index.mjs.map +1 -1
  287. package/dist/jsx/schemaForm/index.native.js +16 -2
  288. package/dist/jsx/schemaForm/index.native.js.map +1 -1
  289. package/package.json +16 -16
  290. package/src/FrappeChartCard.tsx +5 -5
  291. package/src/FrappeForm.routedPage.spec.tsx +2 -2
  292. package/src/FrappeForm.tsx +11 -3
  293. package/src/FrappeFormConnections.tsx +5 -3
  294. package/src/FrappeGuestMeta.spec.tsx +1 -1
  295. package/src/FrappeKPICard.tsx +5 -5
  296. package/src/FrappeList.tsx +4 -6
  297. package/src/FrappeListView.cards.spec.tsx +1 -1
  298. package/src/FrappeListView.permissionDenied.spec.tsx +1 -1
  299. package/src/FrappeListView.seed.spec.tsx +1 -1
  300. package/src/FrappeReportBuilder.stories.tsx +1 -0
  301. package/src/FrappeReportBuilder.tsx +2 -1
  302. package/src/FrappeTable.infiniteLoadError.spec.tsx +2 -2
  303. package/src/FrappeTable.refusedCount.spec.tsx +2 -2
  304. package/src/FrappeTable.tsx +22 -4
  305. package/src/frappeRefusals.spec.tsx +168 -0
  306. package/src/list/listColumns.spec.tsx +3 -3
  307. package/src/list/listFields.spec.ts +2 -2
  308. package/src/liveErrors.ts +22 -0
  309. package/src/report/ReportGrid.phone.spec.tsx +5 -5
  310. package/src/report/ReportGrid.spec.tsx +14 -1
  311. package/src/report/ReportGrid.tsx +24 -3
  312. package/src/report/ReportViewToolbar.spec.tsx +32 -2
  313. package/src/report/ReportViewToolbar.tsx +29 -24
  314. package/src/schemaForm/SchemaForm.flat.spec.tsx +20 -11
  315. package/src/schemaForm/index.tsx +9 -2
  316. package/src/useFrappeRows.lateMount.spec.tsx +1 -1
  317. package/src/useFrappeRows.sharedPath.spec.tsx +2 -1
  318. package/types/FrappeChartCard.d.ts.map +1 -1
  319. package/types/FrappeFormConnections.d.ts.map +1 -1
  320. package/types/FrappeKPICard.d.ts.map +1 -1
  321. package/types/FrappeList.d.ts.map +1 -1
  322. package/types/FrappeReportBuilder.d.ts.map +1 -1
  323. package/types/FrappeTable.d.ts.map +1 -1
  324. package/types/liveErrors.d.ts +6 -0
  325. package/types/liveErrors.d.ts.map +1 -1
  326. package/types/report/ReportGrid.d.ts +5 -0
  327. package/types/report/ReportGrid.d.ts.map +1 -1
  328. package/types/report/ReportViewToolbar.d.ts.map +1 -1
  329. package/types/schemaForm/index.d.ts +1 -0
  330. package/types/schemaForm/index.d.ts.map +1 -1
@@ -35,6 +35,7 @@ import React, { useEffect, useMemo } from "react";
35
35
  import { useTranslation } from "react-i18next";
36
36
  import { YStack, type YStackProps } from "tamagui";
37
37
  import { useResolvedKnobs } from "@multiplatform.one/theme";
38
+ import { failureReason } from "./liveErrors";
38
39
  import { ChartCard } from "./ChartCard";
39
40
  import { useFrappeConfig } from "./FrappeProvider";
40
41
  import { frappeDevWarn } from "./shared/devWarn";
@@ -191,16 +192,15 @@ export function FrappeChartCard({
191
192
  );
192
193
  }
193
194
 
195
+ const errorReason = isPermissionError ? "permission_denied" : failureReason(error);
194
196
  if (error) {
195
197
  return (
196
198
  <AsyncBoundary
197
199
  size="$3"
198
200
  error={error}
199
- errorTitle={
200
- isPermissionError
201
- ? t("No permission to read {{doctype}}", { doctype: dataSource.doctype })
202
- : t("Couldn't load {{title}}", { title: config.title })
203
- }
201
+ reason={errorReason}
202
+ subject={config.title}
203
+ errorTitle={errorReason ? undefined : t("Couldn't load {{title}}", { title: config.title })}
204
204
  {...(chartCardProps as YStackProps)}
205
205
  />
206
206
  );
@@ -29,11 +29,11 @@ const meta = {
29
29
  title_field: "pokemon_name",
30
30
  image_field: "sprite_front",
31
31
  fields: [
32
- { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", reqd: 1 },
32
+ { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon name", reqd: 1 },
33
33
  { fieldname: "height", fieldtype: "Int", label: "Height (dm)" },
34
34
  { fieldname: "weight", fieldtype: "Int", label: "Weight (hg)" },
35
35
  { fieldname: "column_break_1", fieldtype: "Column Break" },
36
- { fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (Front)" },
36
+ { fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (front)" },
37
37
  ],
38
38
  permissions: [{ role: "Pokemon Editor", read: 1, write: 1, create: 1 }],
39
39
  };
@@ -49,7 +49,7 @@ import { connectionFields, connectionsFromMeta, type FrappeConnection } from "./
49
49
  import { FrappeFormConnections } from "./FrappeFormConnections";
50
50
  import { FrappeNotConfigured } from "./FrappeNotConfigured";
51
51
  import { FrappeRowsReader, type FrappeRowsSnapshot } from "./FrappeRowsReader";
52
- import { isPermissionError } from "./liveErrors";
52
+ import { failureReason, isPermissionError } from "./liveErrors";
53
53
  import { fieldsFromRecord } from "./recordShape";
54
54
  import {
55
55
  frappeRouteHref,
@@ -798,7 +798,11 @@ function FrappeFormView<TDoc extends { name: string; doctype: string }>({
798
798
  <AsyncBoundary
799
799
  {...knobProps.panelPadding}
800
800
  error={metaError}
801
- errorTitle={t("Failed to load {{doctype}} form", { doctype })}
801
+ reason={failureReason(metaError)}
802
+ subject={doctype}
803
+ errorTitle={
804
+ failureReason(metaError) ? undefined : t("Failed to load {{doctype}} form", { doctype })
805
+ }
802
806
  />
803
807
  ),
804
808
  );
@@ -819,7 +823,11 @@ function FrappeFormView<TDoc extends { name: string; doctype: string }>({
819
823
  <AsyncBoundary
820
824
  {...knobProps.panelPadding}
821
825
  error={docError}
822
- errorTitle={t("Failed to load {{doctype}}", { doctype })}
826
+ reason={failureReason(docError)}
827
+ subject={doctype}
828
+ errorTitle={
829
+ failureReason(docError) ? undefined : t("Failed to load {{doctype}}", { doctype })
830
+ }
823
831
  />,
824
832
  );
825
833
  }
@@ -5,7 +5,7 @@ import { useMemo } from "react";
5
5
  import { useTranslation } from "react-i18next";
6
6
  import { YStack } from "tamagui";
7
7
  import { connectionFields, type FrappeConnection } from "./frappeConnections";
8
- import { isPermissionError } from "./liveErrors";
8
+ import { failureReason } from "./liveErrors";
9
9
  import { useFrappeRows, type FrappeRowsConnection } from "./useFrappeRows";
10
10
 
11
11
  type ConnectionRow = Record<string, unknown> & { name: string; doctype: string };
@@ -98,9 +98,11 @@ export function FrappeFormConnectionSection({
98
98
  size="$3"
99
99
  loading={isLoading}
100
100
  error={error}
101
+ reason={failureReason(error)}
102
+ subject={link.doctype}
101
103
  errorTitle={
102
- error && isPermissionError(error)
103
- ? t("You do not have permission to view {{doctype}}", { doctype: link.doctype })
104
+ failureReason(error)
105
+ ? undefined
104
106
  : t("Error loading {{doctype}}", { doctype: link.doctype })
105
107
  }
106
108
  empty={rows.length === 0}
@@ -27,7 +27,7 @@ const roster = [
27
27
  const pokemonMeta = {
28
28
  name: "Pokemon",
29
29
  title_field: "pokemon_name",
30
- fields: [{ fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name" }],
30
+ fields: [{ fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon name" }],
31
31
  permissions: [{ role: "Guest", read: 1 }],
32
32
  };
33
33
 
@@ -30,6 +30,7 @@ import {
30
30
  import React, { useEffect, useMemo } from "react";
31
31
  import { useTranslation } from "react-i18next";
32
32
  import { type YStackProps } from "tamagui";
33
+ import { failureReason } from "./liveErrors";
33
34
  import { useFrappeConfig } from "./FrappeProvider";
34
35
  import { frappeDevWarn } from "./shared/devWarn";
35
36
  import {
@@ -161,16 +162,15 @@ export function FrappeKPICard({
161
162
  return <KPICardSkeleton {...stackProps} />;
162
163
  }
163
164
 
165
+ const errorReason = isPermissionError ? "permission_denied" : failureReason(error);
164
166
  if (error) {
165
167
  return (
166
168
  <AsyncBoundary
167
169
  size="$3"
168
170
  error={error}
169
- errorTitle={
170
- isPermissionError
171
- ? t("No permission to read {{doctype}}", { doctype: dataSource.doctype })
172
- : t("Couldn't load {{title}}", { title: config.title })
173
- }
171
+ reason={errorReason}
172
+ subject={config.title}
173
+ errorTitle={errorReason ? undefined : t("Couldn't load {{title}}", { title: config.title })}
174
174
  {...stackProps}
175
175
  />
176
176
  );
@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
12
12
  import { YStack } from "tamagui";
13
13
  import { useFrappeConfig } from "./FrappeProvider";
14
14
  import { FrappeReconnectIndicator } from "./FrappeReconnectIndicator";
15
- import { isPermissionError } from "./liveErrors";
15
+ import { failureReason } from "./liveErrors";
16
16
  import { useFrappePagination, type UseFrappePaginationOptions } from "./useFrappePagination";
17
17
 
18
18
  export interface FrappeListProps<TDoc extends { name: string; doctype: string }> extends Omit<
@@ -135,11 +135,9 @@ export function FrappeList<TDoc extends { name: string; doctype: string }>({
135
135
  layout={layout === "grid" ? "generic" : "list"}
136
136
  error={error}
137
137
  onRetry={onRetry}
138
- errorTitle={
139
- isPermissionError(error)
140
- ? t("You do not have permission to view {{doctype}}", { doctype })
141
- : t("Error loading {{doctype}}", { doctype })
142
- }
138
+ reason={failureReason(error)}
139
+ subject={doctype}
140
+ errorTitle={failureReason(error) ? undefined : t("Error loading {{doctype}}", { doctype })}
143
141
  />
144
142
  );
145
143
  }
@@ -168,7 +168,7 @@ describe("FrappeListView card grid states", () => {
168
168
  const fixtures = createRosterFixtures();
169
169
  vi.spyOn(fixtures, "getList").mockRejectedValue(new Error("Failed to fetch"));
170
170
  renderCards(fixtures, { emptyState: <Text testID="roster-empty">empty</Text> });
171
- await waitFor(() => expect(screen.getByText("Error loading Pokemon")).toBeTruthy());
171
+ await waitFor(() => expect(screen.getByText("Pokemon didn't load")).toBeTruthy());
172
172
  expect(screen.queryByTestId("roster-empty")).toBeNull();
173
173
  expect(screen.getByRole("button", { name: /retry/i })).toBeTruthy();
174
174
  });
@@ -26,7 +26,7 @@ describe("FrappeListView Cards / Permission Denied", () => {
26
26
  it("shows the permission error, not the empty roster", async () => {
27
27
  renderWithProviders(<PermissionDenied />);
28
28
  await waitFor(() =>
29
- expect(screen.getByText("You do not have permission to view Pokemon")).toBeTruthy(),
29
+ expect(screen.getByText("Pokemon isn't open to this account")).toBeTruthy(),
30
30
  );
31
31
  expect(screen.queryByText("No Pokemon records found")).toBeNull();
32
32
  });
@@ -42,7 +42,7 @@ const pokemonMeta = {
42
42
  name: "Pokemon",
43
43
  title_field: "pokemon_name",
44
44
  fields: [
45
- { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", in_list_view: 1 },
45
+ { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon name", in_list_view: 1 },
46
46
  { fieldname: "poke_id", fieldtype: "Int", label: "Poke ID", in_list_view: 1 },
47
47
  ],
48
48
  permissions: [{ role: "Guest", read: 1 }],
@@ -10,6 +10,7 @@ export default {
10
10
  tags: ["!test"],
11
11
  parameters: {
12
12
  status: { type: "stable" },
13
+ layout: "fullscreen",
13
14
  docs: {
14
15
  description: {
15
16
  component:
@@ -868,7 +868,8 @@ function FrappeReportBuilderBody({
868
868
  />
869
869
  </YStack>
870
870
  <ScrollView
871
- flex={1}
871
+ flexGrow={0}
872
+ flexShrink={1}
872
873
  minWidth={0}
873
874
  minHeight={0}
874
875
  paddingHorizontal="$3"
@@ -22,10 +22,10 @@ interface Pokemon {
22
22
 
23
23
  const columns: DataTableColumn<Partial<Pokemon>>[] = [
24
24
  { id: "name", accessorKey: "name", header: "ID" },
25
- { id: "pokemon_name", accessorKey: "pokemon_name", header: "Pokemon Name" },
25
+ { id: "pokemon_name", accessorKey: "pokemon_name", header: "Pokemon name" },
26
26
  ];
27
27
 
28
- const filterFields = [{ id: "pokemon_name", label: "Pokemon Name", type: "text" as const }];
28
+ const filterFields = [{ id: "pokemon_name", label: "Pokemon name", type: "text" as const }];
29
29
  const readOnly = { create: false, write: false, delete: false };
30
30
  const settle = { timeout: 8000 };
31
31
 
@@ -21,10 +21,10 @@ interface Pokemon {
21
21
 
22
22
  const columns: DataTableColumn<Partial<Pokemon>>[] = [
23
23
  { id: "name", accessorKey: "name", header: "ID" },
24
- { id: "pokemon_name", accessorKey: "pokemon_name", header: "Pokemon Name" },
24
+ { id: "pokemon_name", accessorKey: "pokemon_name", header: "Pokemon name" },
25
25
  ];
26
26
 
27
- const filterFields = [{ id: "pokemon_name", label: "Pokemon Name", type: "text" as const }];
27
+ const filterFields = [{ id: "pokemon_name", label: "Pokemon name", type: "text" as const }];
28
28
  const readOnly = { create: false, write: false, delete: false };
29
29
  const settle = { timeout: 8000 };
30
30
 
@@ -29,7 +29,7 @@ import { buildPermLevelMatrix } from "./formEngine/fieldPermissions";
29
29
  import { useDocRights } from "./formEngine/useDocRights";
30
30
  import { buildListColumns } from "./list/listColumns";
31
31
  import { listFieldsToFetch } from "./list/listFields";
32
- import { isPermissionError } from "./liveErrors";
32
+ import { failureReason, isPermissionError } from "./liveErrors";
33
33
  import { useFrappeRows } from "./useFrappeRows";
34
34
 
35
35
  /**
@@ -756,6 +756,12 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
756
756
  // `limit: null` keeps the bench's own page length rather than the hook's
757
757
  // default — FrappeTable paginates on the rows it is handed, and moving its
758
758
  // row ceiling here would move it for every existing consumer.
759
+ // Retry on the live path parks the read for one render: the last release
760
+ // evicts the errored collection, so the next acquire is one fresh sync.
761
+ const [retryParked, setRetryParked] = useState(false);
762
+ useEffect(() => {
763
+ if (retryParked) setRetryParked(false);
764
+ }, [retryParked]);
759
765
  const rowsMessages = useMemo(
760
766
  () => ({
761
767
  permission: t("You do not have permission to view {{doctype}}", { doctype }),
@@ -772,7 +778,7 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
772
778
  fields: clientFields,
773
779
  filters: activeFilters,
774
780
  limit: null,
775
- enabled: !serverSide && !columnsPending,
781
+ enabled: !serverSide && !columnsPending && !retryParked,
776
782
  messages: rowsMessages,
777
783
  });
778
784
 
@@ -854,11 +860,20 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
854
860
  // while a refetch is in flight — that was the named blank-the-list defect.
855
861
  const boundaryState = resolvedError
856
862
  ? { error: resolvedError }
857
- : isRowsLoadingResolved || columnsPending
863
+ : isRowsLoadingResolved || columnsPending || retryParked
858
864
  ? { loading: loading ?? true }
859
865
  : rowData.length === 0 && !infiniteServerMode && !isFetching
860
866
  ? { empty: true }
861
867
  : null;
868
+ const errorReason = failureReason(resolvedError);
869
+ const retryLoad = () => {
870
+ if (!serverSide) {
871
+ setRetryParked(true);
872
+ return;
873
+ }
874
+ lastFetchKey.current = "";
875
+ if (lastStateRef.current) fetchServerData(lastStateRef.current);
876
+ };
862
877
  if (boundaryState) {
863
878
  return (
864
879
  <YStack
@@ -874,7 +889,10 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
874
889
  <FrappeReconnectIndicator config={connectionConfig} />
875
890
  <AsyncBoundary
876
891
  layout={layout === "list" || layout === "cards" ? "list" : "table"}
877
- errorTitle={t("Error loading {{doctype}}", { doctype })}
892
+ reason={errorReason}
893
+ subject={doctype}
894
+ onRetry={resolvedError && !infiniteServerMode ? retryLoad : undefined}
895
+ errorTitle={errorReason ? undefined : t("Error loading {{doctype}}", { doctype })}
878
896
  emptyTitle={t("No {{doctype}} records found", { doctype })}
879
897
  {...boundaryState}
880
898
  />
@@ -0,0 +1,168 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { cleanup, fireEvent, screen } from "@testing-library/react";
3
+ import i18next from "i18next";
4
+ import { initReactI18next } from "react-i18next";
5
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { FrappeChartCard } from "./FrappeChartCard";
7
+ import { FrappeFormConnectionSection } from "./FrappeFormConnections";
8
+ import { FrappeKPICard } from "./FrappeKPICard";
9
+ import { FrappeList } from "./FrappeList";
10
+ import { FrappeProvider } from "./FrappeProvider";
11
+ import { FrappeTable } from "./FrappeTable";
12
+ import { failureReason, toLiveLoadError } from "./liveErrors";
13
+
14
+ const state = vi.hoisted(() => ({ error: null as unknown, permission: false }));
15
+
16
+ vi.mock("./useFrappeRows", async (importOriginal) => {
17
+ const real = await importOriginal<typeof import("./useFrappeRows")>();
18
+ return {
19
+ ...real,
20
+ useFrappeRows: () => ({
21
+ rows: [],
22
+ syncError: state.error,
23
+ isLoading: false,
24
+ isQueryLoading: false,
25
+ isError: state.error !== null,
26
+ error: state.error,
27
+ collection: undefined,
28
+ }),
29
+ };
30
+ });
31
+
32
+ vi.mock("./useFrappePagination", () => ({
33
+ useFrappePagination: () => ({
34
+ items: [],
35
+ isLoading: false,
36
+ onLoadMore: () => {},
37
+ hasMore: false,
38
+ error: state.error,
39
+ }),
40
+ }));
41
+
42
+ vi.mock("./useFrappeAggregate", async (importOriginal) => {
43
+ const real = await importOriginal<typeof import("./useFrappeAggregate")>();
44
+ return {
45
+ ...real,
46
+ useFrappeAggregate: () => ({
47
+ groups: [],
48
+ value: undefined,
49
+ isLoading: false,
50
+ isStale: false,
51
+ error: state.error,
52
+ isPermissionError: state.permission,
53
+ }),
54
+ };
55
+ });
56
+
57
+ beforeAll(async () => {
58
+ await i18next.use(initReactI18next).init({
59
+ lng: "en",
60
+ interpolation: { escapeValue: false },
61
+ resources: { en: { translation: {} } },
62
+ });
63
+ });
64
+
65
+ beforeEach(() => {
66
+ state.error = null;
67
+ state.permission = false;
68
+ });
69
+
70
+ afterEach(cleanup);
71
+
72
+ const host = "https://refusals.mock.test";
73
+ const messages = {
74
+ permissionMessage: "You do not have permission to view Customer",
75
+ genericMessage: "Error loading Customer",
76
+ };
77
+ const refused = () =>
78
+ toLiveLoadError(Object.assign(new Error("Not permitted"), { status: 403 }), messages);
79
+ const unreachable = () => toLiveLoadError(new TypeError("Failed to fetch"), messages);
80
+
81
+ function expectRefusal(container: HTMLElement, subject: string) {
82
+ const node = container.querySelector('[data-async-state="refused"]');
83
+ expect(node).not.toBeNull();
84
+ expect(container.querySelector('[data-empty-intent="neutral"]')).not.toBeNull();
85
+ expect(screen.getByText(`${subject} isn't open to this account`)).toBeTruthy();
86
+ expect(screen.queryByText(/Retry/)).toBeNull();
87
+ expect(container.textContent).not.toMatch(/Not permitted|do not have permission/);
88
+ }
89
+
90
+ const customerColumns = [{ id: "customer_name", header: "Name", accessorKey: "customer_name" }];
91
+
92
+ function table() {
93
+ return renderWithProviders(
94
+ <FrappeProvider baseURL={host}>
95
+ <FrappeTable doctype="Customer" columns={customerColumns as never} />
96
+ </FrappeProvider>,
97
+ );
98
+ }
99
+
100
+ describe("failureReason (FZ-9227)", () => {
101
+ it.each([
102
+ [refused(), "permission_denied"],
103
+ [Object.assign(new Error("x"), { status: 504 }), "timeout"],
104
+ [new Error("Request timed out"), "timeout"],
105
+ [unreachable(), "unreachable"],
106
+ [Object.assign(new Error("x"), { status: 503 }), "down"],
107
+ [Object.assign(new Error("x"), { status: 429 }), "busy"],
108
+ [new Error("Customer CUST-1 not found"), undefined],
109
+ [null, undefined],
110
+ ])("%s -> %s", (error, reason) => {
111
+ expect(failureReason(error)).toBe(reason);
112
+ });
113
+ });
114
+
115
+ describe("frappe-ui refusals render LC-165's neutral state (FZ-9227)", () => {
116
+ it("FrappeTable: a 403 is a neutral refusal with no Retry and no server suffix", () => {
117
+ state.error = refused();
118
+ const { container } = table();
119
+ expectRefusal(container, "Customer");
120
+ });
121
+
122
+ it("FrappeTable: a network failure names the cause and offers Retry, with no exception type", () => {
123
+ state.error = unreachable();
124
+ const { container } = table();
125
+ expect(screen.getByText("Customer didn't load")).toBeTruthy();
126
+ expect(screen.getByText(/Can't reach the server\. Tap Retry to try again\./)).toBeTruthy();
127
+ expect(screen.getByText("Retry")).toBeTruthy();
128
+ expect(container.textContent).not.toMatch(/TypeError|Failed to fetch/);
129
+ fireEvent.click(screen.getByText("Retry"));
130
+ });
131
+
132
+ it("FrappeList: a refusal is neutral", () => {
133
+ state.error = refused();
134
+ const { container } = renderWithProviders(
135
+ <FrappeProvider baseURL={host}>
136
+ <FrappeList doctype="Customer" renderItem={() => null} onRetry={() => {}} />
137
+ </FrappeProvider>,
138
+ );
139
+ expectRefusal(container, "Customer");
140
+ });
141
+
142
+ it.each([
143
+ ["FrappeKPICard", FrappeKPICard],
144
+ ["FrappeChartCard", FrappeChartCard],
145
+ ] as const)("%s: a refusal is neutral", (_name, Card) => {
146
+ state.error = new Error("Not permitted");
147
+ state.permission = true;
148
+ const config = { id: "open", title: "Open ToDos", value: 0, format: "number" as const };
149
+ const { container } = renderWithProviders(
150
+ <FrappeProvider baseURL={host}>
151
+ <Card config={config as never} dataSource={{ doctype: "ToDo" } as never} />
152
+ </FrappeProvider>,
153
+ );
154
+ expectRefusal(container, "Open ToDos");
155
+ });
156
+
157
+ it("FrappeFormConnectionSection: a refusal is neutral", () => {
158
+ state.error = refused();
159
+ const { container } = renderWithProviders(
160
+ <FrappeFormConnectionSection
161
+ connection={{ baseURL: host } as never}
162
+ link={{ doctype: "Sales Invoice", fieldname: "customer" } as never}
163
+ name="CUST-1"
164
+ />,
165
+ );
166
+ expectRefusal(container, "Sales Invoice");
167
+ });
168
+ });
@@ -69,11 +69,11 @@ describe("image field media column", () => {
69
69
  title_field: "pokemon_name",
70
70
  image_field: "sprite_front",
71
71
  fields: [
72
- { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", in_list_view: 1 },
72
+ { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon name", in_list_view: 1 },
73
73
  {
74
74
  fieldname: "sprite_front",
75
75
  fieldtype: "Attach Image",
76
- label: "Sprite (Front)",
76
+ label: "Sprite (front)",
77
77
  in_list_view: 1,
78
78
  },
79
79
  ],
@@ -84,7 +84,7 @@ describe("image field media column", () => {
84
84
  const image = columns.filter((column) => column.id === "sprite_front");
85
85
  expect(image).toHaveLength(1);
86
86
  expect(image[0].meta?.role).toBe("media");
87
- expect(image[0].meta?.label).toBe("Sprite (Front)");
87
+ expect(image[0].meta?.label).toBe("Sprite (front)");
88
88
  });
89
89
 
90
90
  it("joins a relative file path to the site origin", () => {
@@ -153,8 +153,8 @@ describe("listFieldsToFetch image field", () => {
153
153
  title_field: "pokemon_name",
154
154
  image_field: "sprite_front",
155
155
  fields: [
156
- { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", in_list_view: 1 },
157
- { fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (Front)" },
156
+ { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon name", in_list_view: 1 },
157
+ { fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (front)" },
158
158
  ],
159
159
  };
160
160
  expect(listFieldsToFetch(withImage)).toContain("sprite_front");
package/src/liveErrors.ts CHANGED
@@ -21,6 +21,28 @@ export function isPermissionError(error: unknown): boolean {
21
21
  return /permission|not permitted|forbidden/i.test(message);
22
22
  }
23
23
 
24
+ /**
25
+ * The AsyncBoundary reason for a load failure (LC-204, LC-165): a refusal, or
26
+ * a failure family whose copy names the cause and the next step. Undefined
27
+ * when the error fits none, so the caller's own copy stands.
28
+ */
29
+ export function failureReason(error: unknown): string | undefined {
30
+ if (!error || error === true) return undefined;
31
+ if (isPermissionError(error)) return "permission_denied";
32
+ const status = (error as { status?: number }).status;
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ if (status === 408 || status === 504 || /timed? ?out|timeout/i.test(message)) return "timeout";
35
+ if (status === 413) return "too_large";
36
+ if (status === 429) return "busy";
37
+ if (status === 502 || status === 503) return "down";
38
+ if (
39
+ /failed to fetch|network ?error|network request failed|econnrefused|enotfound/i.test(message)
40
+ ) {
41
+ return "unreachable";
42
+ }
43
+ return undefined;
44
+ }
45
+
24
46
  /** True when `detail` is a developer-mode python traceback, not a sentence. */
25
47
  function isTracebackDetail(detail: string): boolean {
26
48
  return /Traceback \(most recent call last\)/i.test(detail);
@@ -15,7 +15,7 @@ import {
15
15
  const columns: ReportColumnDef[] = [
16
16
  {
17
17
  fieldname: "pokemon_name",
18
- label: "Pokemon Name",
18
+ label: "Pokemon name",
19
19
  fieldtype: "Data",
20
20
  kind: "text",
21
21
  width: 160,
@@ -78,7 +78,7 @@ describe("ReportGrid on a phone", () => {
78
78
  it("drops the row numbers and the per-column filter row", () => {
79
79
  renderGrid();
80
80
  expect(screen.queryByLabelText("Row number")).toBeNull();
81
- expect(screen.queryByLabelText("Filter Pokemon Name")).toBeNull();
81
+ expect(screen.queryByLabelText("Filter Pokemon name")).toBeNull();
82
82
  });
83
83
 
84
84
  it("heads a number column without its unit and puts the unit in the cell", () => {
@@ -115,13 +115,13 @@ describe("phoneReportFieldnames", () => {
115
115
  title_field: "pokemon_name",
116
116
  image_field: "sprite_front",
117
117
  fields: [
118
- { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", in_list_view: 1 },
118
+ { fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon name", in_list_view: 1 },
119
119
  { fieldname: "poke_id", fieldtype: "Int", label: "Poke ID", in_list_view: 1 },
120
120
  { fieldname: "species", fieldtype: "Data", label: "Species", in_list_view: 1 },
121
121
  { fieldname: "height", fieldtype: "Int", label: "Height (dm)" },
122
122
  { fieldname: "weight", fieldtype: "Int", label: "Weight (hg)" },
123
- { fieldname: "base_experience", fieldtype: "Int", label: "Base Experience" },
124
- { fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (Front)" },
123
+ { fieldname: "base_experience", fieldtype: "Int", label: "Base experience" },
124
+ { fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (front)" },
125
125
  ],
126
126
  }),
127
127
  ).toEqual(["pokemon_name", "poke_id", "height", "weight"]);
@@ -13,7 +13,7 @@ import { renderWithProviders } from "@multiplatform.one/test-utils";
13
13
  import { fireEvent, screen, waitFor } from "@testing-library/react";
14
14
  import React from "react";
15
15
  import { describe, expect, it, vi } from "vitest";
16
- import { ReportGrid, type ReportGridProps } from "./ReportGrid";
16
+ import { ReportGrid, type ReportGridProps, stretchColumns } from "./ReportGrid";
17
17
  import { buildGridRows, type ReportColumnDef, type ReportRow } from "./reportViewModel";
18
18
 
19
19
  const columns: ReportColumnDef[] = [
@@ -173,3 +173,16 @@ describe("ReportGrid inner interactive content wins", () => {
173
173
  expect(body!.querySelectorAll('[role="button"], button')).toHaveLength(0);
174
174
  });
175
175
  });
176
+
177
+ describe("a wide report fills its viewport (FZ-8907)", () => {
178
+ it("spreads spare width over the columns so the grid meets the viewport edge", () => {
179
+ const stretched = stretchColumns(columns, 56, 1056);
180
+ expect(stretched.reduce((sum, column) => sum + column.width, 0)).toBe(1000);
181
+ expect(stretched[0]!.width).toBeGreaterThan(columns[0]!.width);
182
+ expect(stretched[1]!.width / stretched[0]!.width).toBeCloseTo(120 / 160, 1);
183
+ });
184
+
185
+ it("keeps natural widths when the columns already overflow", () => {
186
+ expect(stretchColumns(columns, 56, 300).map((column) => column.width)).toEqual([160, 120, 160]);
187
+ });
188
+ });
@@ -67,8 +67,8 @@ function fitColumns(
67
67
  ...(column.fieldname === titleFieldname
68
68
  ? { width: { flex: 1, min: "$10" as const } }
69
69
  : REPORT_NUMERIC_FIELDTYPES.has(column.fieldtype)
70
- ? { width: "$8" as const }
71
- : { width: "$11" as const }),
70
+ ? { width: "$10" as const }
71
+ : { width: "$9" as const }),
72
72
  })),
73
73
  data as Array<Record<string, any>>,
74
74
  viewportWidth,
@@ -80,6 +80,25 @@ function fitColumns(
80
80
  }));
81
81
  }
82
82
 
83
+ /**
84
+ * A wide report fills its viewport: spare width goes to every column in
85
+ * proportion, so the grid's right edge meets the toolbar's (FZ-8907).
86
+ */
87
+ export function stretchColumns(
88
+ columns: readonly ReportColumnDef[],
89
+ fixedWidth: number,
90
+ viewportWidth: number,
91
+ ): ReportColumnDef[] {
92
+ const natural = columns.reduce((sum, column) => sum + column.width, 0);
93
+ const room = viewportWidth - fixedWidth;
94
+ if (natural <= 0 || room <= natural) return [...columns];
95
+ const scale = room / natural;
96
+ const widths = columns.map((column) => Math.floor(column.width * scale));
97
+ const last = widths.length - 1;
98
+ widths[last] = (widths[last] ?? 0) + room - widths.reduce((sum, width) => sum + width, 0);
99
+ return columns.map((column, index) => ({ ...column, width: widths[index] ?? column.width }));
100
+ }
101
+
83
102
  // ── LC-49/LC-50 row keyboard contract ───────────────────────────────
84
103
  // Local mirror of the table package's `isInteractiveInnerTarget`
85
104
  // (public/table/src/utils/rowPress.ts — internal util, not on the
@@ -291,8 +310,10 @@ export function ReportGrid({
291
310
  const compact = useLayoutSizeClass() === "compact";
292
311
  const [viewportWidth, setViewportWidth] = useState(0);
293
312
  const titleFieldname = titleField ?? columnsIn[0]?.fieldname;
294
- const columns = compact ? fitColumns(columnsIn, rows, titleFieldname, viewportWidth) : columnsIn;
295
313
  const rowNumberWidth = compact ? 0 : ROW_NUMBER_WIDTH;
314
+ const columns = compact
315
+ ? fitColumns(columnsIn, rows, titleFieldname, viewportWidth)
316
+ : stretchColumns(columnsIn, rowNumberWidth, viewportWidth);
296
317
 
297
318
  const handleSortPress = useCallback(
298
319
  (fieldname: string) => {