@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.
- package/dist/cjs/ChartCard.native.js.map +1 -1
- package/dist/cjs/FileBrowser/AttachLibrary.native.js.map +1 -1
- package/dist/cjs/FileBrowser/folderTree.native.js.map +1 -1
- package/dist/cjs/FileBrowser/index.native.js.map +1 -1
- package/dist/cjs/FileBrowser/types.native.js.map +1 -1
- package/dist/cjs/FileBrowser/viewPersistence.native.js.map +1 -1
- package/dist/cjs/FormSidebar.native.js.map +1 -1
- package/dist/cjs/FrappeAssignment.native.js.map +1 -1
- package/dist/cjs/FrappeCalendar.native.js.map +1 -1
- package/dist/cjs/FrappeChartCard.cjs +5 -3
- package/dist/cjs/FrappeChartCard.native.js +5 -3
- package/dist/cjs/FrappeChartCard.native.js.map +1 -1
- package/dist/cjs/FrappeConnectionIndicator.native.js.map +1 -1
- package/dist/cjs/FrappeDashboard.native.js.map +1 -1
- package/dist/cjs/FrappeForm.cjs +6 -2
- package/dist/cjs/FrappeForm.native.js +7 -3
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeFormActions.native.js.map +1 -1
- package/dist/cjs/FrappeFormConnections.cjs +3 -3
- package/dist/cjs/FrappeFormConnections.native.js +3 -3
- package/dist/cjs/FrappeFormConnections.native.js.map +1 -1
- package/dist/cjs/FrappeGantt.native.js.map +1 -1
- package/dist/cjs/FrappeImageView.native.js.map +1 -1
- package/dist/cjs/FrappeInsights.native.js.map +1 -1
- package/dist/cjs/FrappeKPICard.cjs +5 -3
- package/dist/cjs/FrappeKPICard.native.js +5 -3
- package/dist/cjs/FrappeKPICard.native.js.map +1 -1
- package/dist/cjs/FrappeKanban.native.js.map +1 -1
- package/dist/cjs/FrappeLikedBy.native.js.map +1 -1
- package/dist/cjs/FrappeList.cjs +3 -3
- package/dist/cjs/FrappeList.native.js +3 -3
- package/dist/cjs/FrappeList.native.js.map +1 -1
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeNotConfigured.native.js.map +1 -1
- package/dist/cjs/FrappeProvider.native.js.map +1 -1
- package/dist/cjs/FrappeReconnectIndicator.native.js.map +1 -1
- package/dist/cjs/FrappeRecordView.native.js.map +1 -1
- package/dist/cjs/FrappeReportBuilder.cjs +2 -1
- package/dist/cjs/FrappeReportBuilder.native.js +2 -1
- package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
- package/dist/cjs/FrappeReportView.native.js +3 -3
- package/dist/cjs/FrappeReportView.native.js.map +1 -1
- package/dist/cjs/FrappeRowsReader.native.js.map +1 -1
- package/dist/cjs/FrappeTable.cjs +19 -3
- package/dist/cjs/FrappeTable.native.js +19 -3
- package/dist/cjs/FrappeTable.native.js.map +1 -1
- package/dist/cjs/FrappeTreeView.native.js.map +1 -1
- package/dist/cjs/SyncErrorNotice.native.js.map +1 -1
- package/dist/cjs/commentActions.native.js.map +1 -1
- package/dist/cjs/components/attachments.native.js.map +1 -1
- package/dist/cjs/components/tags.native.js.map +1 -1
- package/dist/cjs/components/timeline.native.js.map +1 -1
- package/dist/cjs/dependsOn.native.js.map +1 -1
- package/dist/cjs/desk/DeskFormPage.native.js.map +1 -1
- package/dist/cjs/desk/DeskFormSidebar.native.js.map +1 -1
- package/dist/cjs/desk/DeskListPage.native.js.map +1 -1
- package/dist/cjs/desk/DeskShell.native.js.map +1 -1
- package/dist/cjs/desk/DeskSidebar.native.js.map +1 -1
- package/dist/cjs/desk/awesomebar.native.js.map +1 -1
- package/dist/cjs/fieldOverrides.native.js.map +1 -1
- package/dist/cjs/fieldRegistry.native.js +31 -31
- package/dist/cjs/fieldRegistry.native.js.map +1 -1
- package/dist/cjs/fields/Barcode/index.native.js.map +1 -1
- package/dist/cjs/fields/Check/index.native.js.map +1 -1
- package/dist/cjs/fields/CodeEditor/index.native.js.map +1 -1
- package/dist/cjs/fields/Color/index.native.js.map +1 -1
- package/dist/cjs/fields/Data/index.native.js.map +1 -1
- package/dist/cjs/fields/Date/catalog.native.js +1 -1
- package/dist/cjs/fields/Date/catalog.native.js.map +1 -1
- package/dist/cjs/fields/Date/index.native.js.map +1 -1
- package/dist/cjs/fields/Date/types.native.js.map +1 -1
- package/dist/cjs/fields/Date/wire.native.js.map +1 -1
- package/dist/cjs/fields/DateRange/index.native.js.map +1 -1
- package/dist/cjs/fields/Duration/index.native.js.map +1 -1
- package/dist/cjs/fields/DynamicLink.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload.native.js.map +1 -1
- package/dist/cjs/fields/FileUploadField/index.native.js.map +1 -1
- package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
- package/dist/cjs/fields/HTML/index.native.js.map +1 -1
- package/dist/cjs/fields/Heading/index.native.js.map +1 -1
- package/dist/cjs/fields/Icon/index.native.js.map +1 -1
- package/dist/cjs/fields/Link.native.js.map +1 -1
- package/dist/cjs/fields/LinkTree.native.js.map +1 -1
- package/dist/cjs/fields/LongText/index.native.js.map +1 -1
- package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
- package/dist/cjs/fields/Numeric/index.native.js.map +1 -1
- package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
- package/dist/cjs/fields/Password/index.native.js.map +1 -1
- package/dist/cjs/fields/Phone/index.native.js.map +1 -1
- package/dist/cjs/fields/Progress/index.native.js.map +1 -1
- package/dist/cjs/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/cjs/fields/Rating/index.native.js.map +1 -1
- package/dist/cjs/fields/ReadOnly/index.native.js.map +1 -1
- package/dist/cjs/fields/Select/index.native.js.map +1 -1
- package/dist/cjs/fields/Signature/index.native.js.map +1 -1
- package/dist/cjs/fields/Slider/index.native.js.map +1 -1
- package/dist/cjs/fields/SmallText/index.native.js.map +1 -1
- package/dist/cjs/fields/Switch/index.native.js.map +1 -1
- package/dist/cjs/fields/TableMultiSelect.native.js.map +1 -1
- package/dist/cjs/fields/TextEditor/types.native.js.map +1 -1
- package/dist/cjs/fields/TimePicker/index.native.js.map +1 -1
- package/dist/cjs/fields/ToggleGroup/index.native.js.map +1 -1
- package/dist/cjs/fields/shared/EditorPreviewPanes.native.js.map +1 -1
- package/dist/cjs/filterFields.native.js.map +1 -1
- package/dist/cjs/formEngine/createPrefill.native.js.map +1 -1
- package/dist/cjs/formEngine/dirtyTracking.native.js.map +1 -1
- package/dist/cjs/formEngine/docLifecycle.native.js.map +1 -1
- package/dist/cjs/formEngine/docPermissions.native.js.map +1 -1
- package/dist/cjs/formEngine/doctypeMeta.native.js.map +1 -1
- package/dist/cjs/formEngine/fetchFrom.native.js.map +1 -1
- package/dist/cjs/formEngine/fieldPermissions.native.js.map +1 -1
- package/dist/cjs/formEngine/formActions.native.js.map +1 -1
- package/dist/cjs/formEngine/formLayout.native.js.map +1 -1
- package/dist/cjs/formEngine/index.native.js.map +1 -1
- package/dist/cjs/formEngine/useAmendedAs.native.js.map +1 -1
- package/dist/cjs/formEngine/useDocRights.native.js.map +1 -1
- package/dist/cjs/formEngine/useFormEngine.native.js.map +1 -1
- package/dist/cjs/formEngine/useSessionRoles.native.js.map +1 -1
- package/dist/cjs/frappeConnections.native.js.map +1 -1
- package/dist/cjs/frappeEventColor.native.js.map +1 -1
- package/dist/cjs/frappeNavigation.native.js.map +1 -1
- package/dist/cjs/frappeStoryTarget.native.js.map +1 -1
- package/dist/cjs/frappeUsers.native.js.map +1 -1
- package/dist/cjs/index.native.js +34 -34
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/insights/InsightsFacetTags.native.js.map +1 -1
- package/dist/cjs/insights/InsightsKpiBand.native.js.map +1 -1
- package/dist/cjs/insights/InsightsLeaders.native.js.map +1 -1
- package/dist/cjs/insights/InsightsRecords.native.js.map +1 -1
- package/dist/cjs/insights/InsightsTable.native.js.map +1 -1
- package/dist/cjs/insights/insightsData.native.js.map +1 -1
- package/dist/cjs/insights/insightsRecord.native.js.map +1 -1
- package/dist/cjs/list/BandFilters.native.js.map +1 -1
- package/dist/cjs/list/FacetBadge.native.js.map +1 -1
- package/dist/cjs/list/FacetList.native.js.map +1 -1
- package/dist/cjs/list/FacetRail.native.js.map +1 -1
- package/dist/cjs/list/ImageWell.native.js.map +1 -1
- package/dist/cjs/list/ListControls.native.js.map +1 -1
- package/dist/cjs/list/ListControlsSheet.native.js.map +1 -1
- package/dist/cjs/list/ListFilterPanel.native.js.map +1 -1
- package/dist/cjs/list/ListFiltersButton.native.js.map +1 -1
- package/dist/cjs/list/ListSidebarToolbar.native.js.map +1 -1
- package/dist/cjs/list/ListToastHost.native.js.map +1 -1
- package/dist/cjs/list/ListToolsSheet.native.js.map +1 -1
- package/dist/cjs/list/RangeFilter.native.js.map +1 -1
- package/dist/cjs/list/ShufflePanel.native.js.map +1 -1
- package/dist/cjs/list/defaultProjection.native.js.map +1 -1
- package/dist/cjs/list/facetBadgeSteps.native.js.map +1 -1
- package/dist/cjs/list/facetReading.native.js.map +1 -1
- package/dist/cjs/list/favorites.native.js.map +1 -1
- package/dist/cjs/list/listColumns.native.js.map +1 -1
- package/dist/cjs/list/listControls.fixture.native.js.map +1 -1
- package/dist/cjs/list/listFields.native.js.map +1 -1
- package/dist/cjs/list/listFormat.native.js.map +1 -1
- package/dist/cjs/list/listState.native.js.map +1 -1
- package/dist/cjs/list/recordLook.native.js.map +1 -1
- package/dist/cjs/list/statusColors.native.js.map +1 -1
- package/dist/cjs/list/useListControls.native.js.map +1 -1
- package/dist/cjs/liveErrors.cjs +15 -0
- package/dist/cjs/liveErrors.native.js +15 -0
- package/dist/cjs/liveErrors.native.js.map +1 -1
- package/dist/cjs/measures.native.js.map +1 -1
- package/dist/cjs/record/RecordBars.native.js.map +1 -1
- package/dist/cjs/record/RecordGallery.native.js.map +1 -1
- package/dist/cjs/record/RecordHero.native.js.map +1 -1
- package/dist/cjs/record/RecordPage.native.js.map +1 -1
- package/dist/cjs/record/RecordPager.native.js.map +1 -1
- package/dist/cjs/record/RecordRows.native.js.map +1 -1
- package/dist/cjs/record/recordData.native.js.map +1 -1
- package/dist/cjs/record/recordSections.native.js.map +1 -1
- package/dist/cjs/recordShape.native.js.map +1 -1
- package/dist/cjs/report/ReportGrid.cjs +18 -4
- package/dist/cjs/report/ReportGrid.native.js +28 -4
- package/dist/cjs/report/ReportGrid.native.js.map +1 -1
- package/dist/cjs/report/ReportViewToolbar.cjs +8 -13
- package/dist/cjs/report/ReportViewToolbar.native.js +8 -13
- package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -1
- package/dist/cjs/report/exportCsv.native.js.map +1 -1
- package/dist/cjs/report/queryReport.native.js.map +1 -1
- package/dist/cjs/report/queryReportColumns.native.js.map +1 -1
- package/dist/cjs/report/reportViewModel.native.js.map +1 -1
- package/dist/cjs/report/savedReports.native.js.map +1 -1
- package/dist/cjs/report/useFrappeReport.native.js.map +1 -1
- package/dist/cjs/report/useQueryReport.native.js.map +1 -1
- package/dist/cjs/resolveControl.native.js.map +1 -1
- package/dist/cjs/schemaForm/SchemaWizard.native.js.map +1 -1
- package/dist/cjs/schemaForm/applyFieldOrder.native.js.map +1 -1
- package/dist/cjs/schemaForm/index.cjs +12 -2
- package/dist/cjs/schemaForm/index.native.js +17 -3
- package/dist/cjs/schemaForm/index.native.js.map +1 -1
- package/dist/cjs/sessionUser.native.js.map +1 -1
- package/dist/cjs/shared/devWarn.native.js.map +1 -1
- package/dist/cjs/shared/useKeyboardInset.native.js.map +1 -1
- package/dist/cjs/syncError.native.js.map +1 -1
- package/dist/cjs/useConnectionFilters.native.js.map +1 -1
- package/dist/cjs/useFrappeAggregate.native.js.map +1 -1
- package/dist/cjs/useFrappeFacets.native.js.map +1 -1
- package/dist/cjs/useFrappeForm.native.js.map +1 -1
- package/dist/cjs/useFrappePagination.native.js.map +1 -1
- package/dist/cjs/useFrappeRows.native.js.map +1 -1
- package/dist/esm/FrappeChartCard.mjs +5 -3
- package/dist/esm/FrappeChartCard.mjs.map +1 -1
- package/dist/esm/FrappeChartCard.native.js +5 -3
- package/dist/esm/FrappeChartCard.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +7 -3
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +7 -3
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeFormConnections.mjs +4 -4
- package/dist/esm/FrappeFormConnections.mjs.map +1 -1
- package/dist/esm/FrappeFormConnections.native.js +4 -4
- package/dist/esm/FrappeFormConnections.native.js.map +1 -1
- package/dist/esm/FrappeKPICard.mjs +5 -3
- package/dist/esm/FrappeKPICard.mjs.map +1 -1
- package/dist/esm/FrappeKPICard.native.js +5 -3
- package/dist/esm/FrappeKPICard.native.js.map +1 -1
- package/dist/esm/FrappeList.mjs +4 -4
- package/dist/esm/FrappeList.mjs.map +1 -1
- package/dist/esm/FrappeList.native.js +4 -4
- package/dist/esm/FrappeList.native.js.map +1 -1
- package/dist/esm/FrappeReportBuilder.mjs +2 -1
- package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
- package/dist/esm/FrappeReportBuilder.native.js +2 -1
- package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
- package/dist/esm/FrappeTable.mjs +20 -4
- package/dist/esm/FrappeTable.mjs.map +1 -1
- package/dist/esm/FrappeTable.native.js +20 -4
- package/dist/esm/FrappeTable.native.js.map +1 -1
- package/dist/esm/liveErrors.mjs +15 -1
- package/dist/esm/liveErrors.mjs.map +1 -1
- package/dist/esm/liveErrors.native.js +15 -1
- package/dist/esm/liveErrors.native.js.map +1 -1
- package/dist/esm/report/ReportGrid.mjs +17 -4
- package/dist/esm/report/ReportGrid.mjs.map +1 -1
- package/dist/esm/report/ReportGrid.native.js +27 -4
- package/dist/esm/report/ReportGrid.native.js.map +1 -1
- package/dist/esm/report/ReportViewToolbar.mjs +8 -13
- package/dist/esm/report/ReportViewToolbar.mjs.map +1 -1
- package/dist/esm/report/ReportViewToolbar.native.js +8 -13
- package/dist/esm/report/ReportViewToolbar.native.js.map +1 -1
- package/dist/esm/schemaForm/index.mjs +12 -3
- package/dist/esm/schemaForm/index.mjs.map +1 -1
- package/dist/esm/schemaForm/index.native.js +16 -3
- package/dist/esm/schemaForm/index.native.js.map +1 -1
- package/dist/jsx/FrappeChartCard.mjs +5 -3
- package/dist/jsx/FrappeChartCard.mjs.map +1 -1
- package/dist/jsx/FrappeChartCard.native.js +5 -3
- package/dist/jsx/FrappeChartCard.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +7 -3
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +6 -2
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeFormConnections.mjs +4 -4
- package/dist/jsx/FrappeFormConnections.mjs.map +1 -1
- package/dist/jsx/FrappeFormConnections.native.js +3 -3
- package/dist/jsx/FrappeFormConnections.native.js.map +1 -1
- package/dist/jsx/FrappeKPICard.mjs +5 -3
- package/dist/jsx/FrappeKPICard.mjs.map +1 -1
- package/dist/jsx/FrappeKPICard.native.js +5 -3
- package/dist/jsx/FrappeKPICard.native.js.map +1 -1
- package/dist/jsx/FrappeList.mjs +4 -4
- package/dist/jsx/FrappeList.mjs.map +1 -1
- package/dist/jsx/FrappeList.native.js +3 -3
- package/dist/jsx/FrappeList.native.js.map +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs +2 -1
- package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
- package/dist/jsx/FrappeReportBuilder.native.js +2 -1
- package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
- package/dist/jsx/FrappeTable.mjs +20 -4
- package/dist/jsx/FrappeTable.mjs.map +1 -1
- package/dist/jsx/FrappeTable.native.js +19 -3
- package/dist/jsx/FrappeTable.native.js.map +1 -1
- package/dist/jsx/liveErrors.mjs +15 -1
- package/dist/jsx/liveErrors.mjs.map +1 -1
- package/dist/jsx/liveErrors.native.js +15 -0
- package/dist/jsx/liveErrors.native.js.map +1 -1
- package/dist/jsx/report/ReportGrid.mjs +17 -4
- package/dist/jsx/report/ReportGrid.mjs.map +1 -1
- package/dist/jsx/report/ReportGrid.native.js +28 -4
- package/dist/jsx/report/ReportGrid.native.js.map +1 -1
- package/dist/jsx/report/ReportViewToolbar.mjs +8 -13
- package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -1
- package/dist/jsx/report/ReportViewToolbar.native.js +8 -13
- package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -1
- package/dist/jsx/schemaForm/index.mjs +12 -3
- package/dist/jsx/schemaForm/index.mjs.map +1 -1
- package/dist/jsx/schemaForm/index.native.js +16 -2
- package/dist/jsx/schemaForm/index.native.js.map +1 -1
- package/package.json +16 -16
- package/src/FrappeChartCard.tsx +5 -5
- package/src/FrappeForm.routedPage.spec.tsx +2 -2
- package/src/FrappeForm.tsx +11 -3
- package/src/FrappeFormConnections.tsx +5 -3
- package/src/FrappeGuestMeta.spec.tsx +1 -1
- package/src/FrappeKPICard.tsx +5 -5
- package/src/FrappeList.tsx +4 -6
- package/src/FrappeListView.cards.spec.tsx +1 -1
- package/src/FrappeListView.permissionDenied.spec.tsx +1 -1
- package/src/FrappeListView.seed.spec.tsx +1 -1
- package/src/FrappeReportBuilder.stories.tsx +1 -0
- package/src/FrappeReportBuilder.tsx +2 -1
- package/src/FrappeTable.infiniteLoadError.spec.tsx +2 -2
- package/src/FrappeTable.refusedCount.spec.tsx +2 -2
- package/src/FrappeTable.tsx +22 -4
- package/src/frappeRefusals.spec.tsx +168 -0
- package/src/list/listColumns.spec.tsx +3 -3
- package/src/list/listFields.spec.ts +2 -2
- package/src/liveErrors.ts +22 -0
- package/src/report/ReportGrid.phone.spec.tsx +5 -5
- package/src/report/ReportGrid.spec.tsx +14 -1
- package/src/report/ReportGrid.tsx +24 -3
- package/src/report/ReportViewToolbar.spec.tsx +32 -2
- package/src/report/ReportViewToolbar.tsx +29 -24
- package/src/schemaForm/SchemaForm.flat.spec.tsx +20 -11
- package/src/schemaForm/index.tsx +9 -2
- package/src/useFrappeRows.lateMount.spec.tsx +1 -1
- package/src/useFrappeRows.sharedPath.spec.tsx +2 -1
- package/types/FrappeChartCard.d.ts.map +1 -1
- package/types/FrappeFormConnections.d.ts.map +1 -1
- package/types/FrappeKPICard.d.ts.map +1 -1
- package/types/FrappeList.d.ts.map +1 -1
- package/types/FrappeReportBuilder.d.ts.map +1 -1
- package/types/FrappeTable.d.ts.map +1 -1
- package/types/liveErrors.d.ts +6 -0
- package/types/liveErrors.d.ts.map +1 -1
- package/types/report/ReportGrid.d.ts +5 -0
- package/types/report/ReportGrid.d.ts.map +1 -1
- package/types/report/ReportViewToolbar.d.ts.map +1 -1
- package/types/schemaForm/index.d.ts +1 -0
- package/types/schemaForm/index.d.ts.map +1 -1
package/src/FrappeChartCard.tsx
CHANGED
|
@@ -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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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 (
|
|
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
|
};
|
package/src/FrappeForm.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
103
|
-
?
|
|
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
|
|
30
|
+
fields: [{ fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon name" }],
|
|
31
31
|
permissions: [{ role: "Guest", read: 1 }],
|
|
32
32
|
};
|
|
33
33
|
|
package/src/FrappeKPICard.tsx
CHANGED
|
@@ -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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
);
|
package/src/FrappeList.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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("
|
|
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("
|
|
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
|
|
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 }],
|
|
@@ -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
|
|
25
|
+
{ id: "pokemon_name", accessorKey: "pokemon_name", header: "Pokemon name" },
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
const filterFields = [{ id: "pokemon_name", label: "Pokemon
|
|
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
|
|
24
|
+
{ id: "pokemon_name", accessorKey: "pokemon_name", header: "Pokemon name" },
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
-
const filterFields = [{ id: "pokemon_name", label: "Pokemon
|
|
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
|
|
package/src/FrappeTable.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
157
|
-
{ fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
124
|
-
{ fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (
|
|
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: "$
|
|
71
|
-
: { width: "$
|
|
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) => {
|