@multiplatform.one/frappe-ui 7.11.0 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cjs/FrappeCalendar.cjs +1 -0
- package/dist/cjs/FrappeCalendar.native.js +1 -0
- package/dist/cjs/FrappeCalendar.native.js.map +1 -1
- package/dist/cjs/FrappeDashboard.cjs +8 -10
- package/dist/cjs/FrappeDashboard.native.js +8 -14
- package/dist/cjs/FrappeDashboard.native.js.map +1 -1
- package/dist/cjs/FrappeForm.cjs +465 -175
- package/dist/cjs/FrappeForm.native.js +611 -207
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeFormConnections.cjs +125 -0
- package/dist/cjs/FrappeFormConnections.native.js +149 -0
- package/dist/cjs/FrappeFormConnections.native.js.map +1 -0
- package/dist/cjs/FrappeImageView.cjs +7 -9
- package/dist/cjs/FrappeImageView.native.js +7 -13
- package/dist/cjs/FrappeImageView.native.js.map +1 -1
- package/dist/cjs/FrappeList.cjs +29 -4
- package/dist/cjs/FrappeList.native.js +37 -5
- package/dist/cjs/FrappeList.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +102 -29
- package/dist/cjs/FrappeListView.native.js +109 -29
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeProvider.cjs +7 -2
- package/dist/cjs/FrappeProvider.native.js +7 -2
- package/dist/cjs/FrappeProvider.native.js.map +1 -1
- package/dist/cjs/FrappeReportBuilder.cjs +10 -11
- package/dist/cjs/FrappeReportBuilder.native.js +10 -16
- package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
- package/dist/cjs/FrappeRowsReader.cjs +49 -0
- package/dist/cjs/FrappeRowsReader.native.js +53 -0
- package/dist/cjs/FrappeRowsReader.native.js.map +1 -0
- package/dist/cjs/FrappeTable.cjs +18 -6
- package/dist/cjs/FrappeTable.native.js +24 -6
- package/dist/cjs/FrappeTable.native.js.map +1 -1
- package/dist/cjs/fieldRegistry.cjs +36 -11
- package/dist/cjs/fieldRegistry.native.js +59 -11
- package/dist/cjs/fieldRegistry.native.js.map +1 -1
- package/dist/cjs/fields/Date/index.native.js +5 -2
- package/dist/cjs/fields/Date/index.native.js.map +1 -1
- package/dist/cjs/fields/Date/sheetKeyboard.cjs +2 -2
- package/dist/cjs/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/cjs/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/cjs/fields/DateRange/index.cjs +2 -1
- package/dist/cjs/fields/DateRange/index.native.js +4 -1
- package/dist/cjs/fields/DateRange/index.native.js.map +1 -1
- package/dist/cjs/fields/Password/index.cjs +1 -1
- package/dist/cjs/fields/Password/index.native.js +1 -1
- package/dist/cjs/formEngine/index.cjs +4 -2
- package/dist/cjs/formEngine/index.native.js +3 -1
- package/dist/cjs/formEngine/index.native.js.map +1 -1
- package/dist/cjs/formEngine/useSessionRoles.cjs +77 -0
- package/dist/cjs/formEngine/useSessionRoles.native.js +84 -0
- package/dist/cjs/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/cjs/frappeConnections.cjs +41 -0
- package/dist/cjs/frappeConnections.native.js +50 -0
- package/dist/cjs/frappeConnections.native.js.map +1 -0
- package/dist/cjs/frappeNavigation.cjs +107 -0
- package/dist/cjs/frappeNavigation.native.js +112 -0
- package/dist/cjs/frappeNavigation.native.js.map +1 -0
- package/dist/cjs/index.cjs +18 -1
- package/dist/cjs/index.native.js +18 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/schemaForm/index.cjs +4 -1
- package/dist/cjs/schemaForm/index.native.js +4 -1
- package/dist/cjs/schemaForm/index.native.js.map +1 -1
- package/dist/cjs/useConnectionFilters.cjs +146 -0
- package/dist/cjs/useConnectionFilters.native.js +213 -0
- package/dist/cjs/useConnectionFilters.native.js.map +1 -0
- package/dist/cjs/useFrappePagination.cjs +37 -18
- package/dist/cjs/useFrappePagination.native.js +59 -23
- package/dist/cjs/useFrappePagination.native.js.map +1 -1
- package/dist/cjs/useFrappeRows.native.js.map +1 -1
- package/dist/esm/FrappeCalendar.mjs +1 -0
- package/dist/esm/FrappeCalendar.mjs.map +1 -1
- package/dist/esm/FrappeCalendar.native.js +1 -0
- package/dist/esm/FrappeCalendar.native.js.map +1 -1
- package/dist/esm/FrappeDashboard.mjs +9 -11
- package/dist/esm/FrappeDashboard.mjs.map +1 -1
- package/dist/esm/FrappeDashboard.native.js +9 -15
- package/dist/esm/FrappeDashboard.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +471 -181
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +599 -195
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeFormConnections.mjs +99 -0
- package/dist/esm/FrappeFormConnections.mjs.map +1 -0
- package/dist/esm/FrappeFormConnections.native.js +120 -0
- package/dist/esm/FrappeFormConnections.native.js.map +1 -0
- package/dist/esm/FrappeImageView.mjs +8 -10
- package/dist/esm/FrappeImageView.mjs.map +1 -1
- package/dist/esm/FrappeImageView.native.js +8 -14
- package/dist/esm/FrappeImageView.native.js.map +1 -1
- package/dist/esm/FrappeList.mjs +31 -6
- package/dist/esm/FrappeList.mjs.map +1 -1
- package/dist/esm/FrappeList.native.js +39 -7
- package/dist/esm/FrappeList.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +103 -30
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +110 -30
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/FrappeProvider.mjs +7 -2
- package/dist/esm/FrappeProvider.mjs.map +1 -1
- package/dist/esm/FrappeProvider.native.js +7 -2
- package/dist/esm/FrappeProvider.native.js.map +1 -1
- package/dist/esm/FrappeReportBuilder.mjs +11 -12
- package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
- package/dist/esm/FrappeReportBuilder.native.js +11 -17
- package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
- package/dist/esm/FrappeRowsReader.mjs +24 -0
- package/dist/esm/FrappeRowsReader.mjs.map +1 -0
- package/dist/esm/FrappeRowsReader.native.js +25 -0
- package/dist/esm/FrappeRowsReader.native.js.map +1 -0
- package/dist/esm/FrappeTable.mjs +18 -6
- package/dist/esm/FrappeTable.mjs.map +1 -1
- package/dist/esm/FrappeTable.native.js +24 -6
- package/dist/esm/FrappeTable.native.js.map +1 -1
- package/dist/esm/fieldRegistry.mjs +35 -12
- package/dist/esm/fieldRegistry.mjs.map +1 -1
- package/dist/esm/fieldRegistry.native.js +58 -12
- package/dist/esm/fieldRegistry.native.js.map +1 -1
- package/dist/esm/fields/Date/index.native.js +6 -3
- package/dist/esm/fields/Date/index.native.js.map +1 -1
- package/dist/esm/fields/Date/sheetKeyboard.mjs +2 -2
- package/dist/esm/fields/Date/sheetKeyboard.mjs.map +1 -1
- package/dist/esm/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/esm/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/esm/fields/DateRange/index.mjs +2 -1
- package/dist/esm/fields/DateRange/index.mjs.map +1 -1
- package/dist/esm/fields/DateRange/index.native.js +4 -1
- package/dist/esm/fields/DateRange/index.native.js.map +1 -1
- package/dist/esm/fields/Password/index.mjs +1 -1
- package/dist/esm/fields/Password/index.mjs.map +1 -1
- package/dist/esm/fields/Password/index.native.js +1 -1
- package/dist/esm/fields/Password/index.native.js.map +1 -1
- package/dist/esm/formEngine/index.mjs +2 -1
- package/dist/esm/formEngine/index.mjs.map +1 -1
- package/dist/esm/formEngine/index.native.js +2 -1
- package/dist/esm/formEngine/index.native.js.map +1 -1
- package/dist/esm/formEngine/useSessionRoles.mjs +52 -0
- package/dist/esm/formEngine/useSessionRoles.mjs.map +1 -0
- package/dist/esm/formEngine/useSessionRoles.native.js +56 -0
- package/dist/esm/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/esm/frappeConnections.mjs +15 -0
- package/dist/esm/frappeConnections.mjs.map +1 -0
- package/dist/esm/frappeConnections.native.js +21 -0
- package/dist/esm/frappeConnections.native.js.map +1 -0
- package/dist/esm/frappeNavigation.mjs +78 -0
- package/dist/esm/frappeNavigation.mjs.map +1 -0
- package/dist/esm/frappeNavigation.native.js +80 -0
- package/dist/esm/frappeNavigation.native.js.map +1 -0
- package/dist/esm/index.mjs +7 -3
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +7 -3
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/schemaForm/index.mjs +4 -1
- package/dist/esm/schemaForm/index.mjs.map +1 -1
- package/dist/esm/schemaForm/index.native.js +4 -1
- package/dist/esm/schemaForm/index.native.js.map +1 -1
- package/dist/esm/useConnectionFilters.mjs +121 -0
- package/dist/esm/useConnectionFilters.mjs.map +1 -0
- package/dist/esm/useConnectionFilters.native.js +185 -0
- package/dist/esm/useConnectionFilters.native.js.map +1 -0
- package/dist/esm/useFrappePagination.mjs +38 -19
- package/dist/esm/useFrappePagination.mjs.map +1 -1
- package/dist/esm/useFrappePagination.native.js +60 -24
- package/dist/esm/useFrappePagination.native.js.map +1 -1
- package/dist/esm/useFrappeRows.mjs.map +1 -1
- package/dist/esm/useFrappeRows.native.js.map +1 -1
- package/dist/jsx/FrappeCalendar.mjs +1 -0
- package/dist/jsx/FrappeCalendar.mjs.map +1 -1
- package/dist/jsx/FrappeCalendar.native.js +1 -0
- package/dist/jsx/FrappeCalendar.native.js.map +1 -1
- package/dist/jsx/FrappeDashboard.mjs +9 -11
- package/dist/jsx/FrappeDashboard.mjs.map +1 -1
- package/dist/jsx/FrappeDashboard.native.js +8 -14
- package/dist/jsx/FrappeDashboard.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +471 -181
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +611 -207
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeFormConnections.mjs +99 -0
- package/dist/jsx/FrappeFormConnections.mjs.map +1 -0
- package/dist/jsx/FrappeFormConnections.native.js +149 -0
- package/dist/jsx/FrappeFormConnections.native.js.map +1 -0
- package/dist/jsx/FrappeImageView.mjs +8 -10
- package/dist/jsx/FrappeImageView.mjs.map +1 -1
- package/dist/jsx/FrappeImageView.native.js +7 -13
- package/dist/jsx/FrappeImageView.native.js.map +1 -1
- package/dist/jsx/FrappeList.mjs +31 -6
- package/dist/jsx/FrappeList.mjs.map +1 -1
- package/dist/jsx/FrappeList.native.js +37 -5
- package/dist/jsx/FrappeList.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +103 -30
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +109 -29
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/FrappeProvider.mjs +7 -2
- package/dist/jsx/FrappeProvider.mjs.map +1 -1
- package/dist/jsx/FrappeProvider.native.js +7 -2
- package/dist/jsx/FrappeProvider.native.js.map +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs +11 -12
- package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
- package/dist/jsx/FrappeReportBuilder.native.js +10 -16
- package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
- package/dist/jsx/FrappeRowsReader.mjs +24 -0
- package/dist/jsx/FrappeRowsReader.mjs.map +1 -0
- package/dist/jsx/FrappeRowsReader.native.js +53 -0
- package/dist/jsx/FrappeRowsReader.native.js.map +1 -0
- package/dist/jsx/FrappeTable.mjs +18 -6
- package/dist/jsx/FrappeTable.mjs.map +1 -1
- package/dist/jsx/FrappeTable.native.js +24 -6
- package/dist/jsx/FrappeTable.native.js.map +1 -1
- package/dist/jsx/fieldRegistry.mjs +35 -12
- package/dist/jsx/fieldRegistry.mjs.map +1 -1
- package/dist/jsx/fieldRegistry.native.js +59 -11
- package/dist/jsx/fieldRegistry.native.js.map +1 -1
- package/dist/jsx/fields/Date/index.native.js +5 -2
- package/dist/jsx/fields/Date/index.native.js.map +1 -1
- package/dist/jsx/fields/Date/sheetKeyboard.mjs +2 -2
- package/dist/jsx/fields/Date/sheetKeyboard.mjs.map +1 -1
- package/dist/jsx/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/jsx/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/jsx/fields/DateRange/index.mjs +2 -1
- package/dist/jsx/fields/DateRange/index.mjs.map +1 -1
- package/dist/jsx/fields/DateRange/index.native.js +4 -1
- package/dist/jsx/fields/DateRange/index.native.js.map +1 -1
- package/dist/jsx/fields/Password/index.mjs +1 -1
- package/dist/jsx/fields/Password/index.mjs.map +1 -1
- package/dist/jsx/fields/Password/index.native.js +1 -1
- package/dist/jsx/fields/Password/index.native.js.map +1 -1
- package/dist/jsx/formEngine/index.mjs +2 -1
- package/dist/jsx/formEngine/index.mjs.map +1 -1
- package/dist/jsx/formEngine/index.native.js +3 -1
- package/dist/jsx/formEngine/index.native.js.map +1 -1
- package/dist/jsx/formEngine/useSessionRoles.mjs +52 -0
- package/dist/jsx/formEngine/useSessionRoles.mjs.map +1 -0
- package/dist/jsx/formEngine/useSessionRoles.native.js +84 -0
- package/dist/jsx/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/jsx/frappeConnections.mjs +15 -0
- package/dist/jsx/frappeConnections.mjs.map +1 -0
- package/dist/jsx/frappeConnections.native.js +50 -0
- package/dist/jsx/frappeConnections.native.js.map +1 -0
- package/dist/jsx/frappeNavigation.mjs +78 -0
- package/dist/jsx/frappeNavigation.mjs.map +1 -0
- package/dist/jsx/frappeNavigation.native.js +112 -0
- package/dist/jsx/frappeNavigation.native.js.map +1 -0
- package/dist/jsx/index.js +7 -3
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +7 -3
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +18 -1
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/schemaForm/index.mjs +4 -1
- package/dist/jsx/schemaForm/index.mjs.map +1 -1
- package/dist/jsx/schemaForm/index.native.js +4 -1
- package/dist/jsx/schemaForm/index.native.js.map +1 -1
- package/dist/jsx/useConnectionFilters.mjs +121 -0
- package/dist/jsx/useConnectionFilters.mjs.map +1 -0
- package/dist/jsx/useConnectionFilters.native.js +213 -0
- package/dist/jsx/useConnectionFilters.native.js.map +1 -0
- package/dist/jsx/useFrappePagination.mjs +38 -19
- package/dist/jsx/useFrappePagination.mjs.map +1 -1
- package/dist/jsx/useFrappePagination.native.js +59 -23
- package/dist/jsx/useFrappePagination.native.js.map +1 -1
- package/dist/jsx/useFrappeRows.mjs.map +1 -1
- package/dist/jsx/useFrappeRows.native.js.map +1 -1
- package/package.json +24 -20
- package/src/FileBrowser/AttachLibrary.stories.tsx +23 -0
- package/src/FrappeCalendar.radius.spec.tsx +55 -0
- package/src/FrappeCalendar.tsx +1 -0
- package/src/FrappeDashboard.tsx +11 -16
- package/src/FrappeForm.connections.spec.tsx +280 -0
- package/src/FrappeForm.connections.stories.tsx +93 -0
- package/src/FrappeForm.deskSemantics.spec.tsx +284 -0
- package/src/FrappeForm.tsx +560 -145
- package/src/FrappeFormConnections.tsx +148 -0
- package/src/FrappeGuestMeta.spec.tsx +143 -0
- package/src/FrappeImageView.tsx +8 -12
- package/src/FrappeList.tsx +71 -15
- package/src/FrappeListView.cards.spec.tsx +175 -0
- package/src/FrappeListView.cards.stories.tsx +131 -0
- package/src/FrappeListView.connections.spec.tsx +192 -0
- package/src/FrappeListView.connections.stories.tsx +99 -0
- package/src/FrappeListView.seed.spec.tsx +228 -0
- package/src/FrappeListView.tsx +177 -31
- package/src/FrappeProvider.tsx +13 -1
- package/src/FrappeReportBuilder.tsx +11 -19
- package/src/FrappeRowsReader.tsx +38 -0
- package/src/FrappeTable.optionalProps.spec.tsx +249 -0
- package/src/FrappeTable.spec.tsx +32 -42
- package/src/FrappeTable.stories.spec.tsx +55 -0
- package/src/FrappeTable.stories.tsx +28 -37
- package/src/FrappeTable.tsx +108 -45
- package/src/childTableDependsOn.spec.tsx +85 -0
- package/src/desk/DeskFormSidebar.spec.tsx +20 -0
- package/src/desk/DeskFormSidebar.stories.tsx +42 -0
- package/src/fieldRegistry.spec.tsx +27 -64
- package/src/fieldRegistry.ts +71 -14
- package/src/fields/Date/index.native.tsx +4 -2
- package/src/fields/Date/sheetKeyboard.spec.ts +14 -5
- package/src/fields/Date/sheetKeyboard.ts +12 -4
- package/src/fields/DateRange/DateRange.spec.tsx +67 -0
- package/src/fields/DateRange/index.tsx +5 -1
- package/src/fields/LinkTree.spec.tsx +8 -0
- package/src/fields/LinkTree.stories.tsx +28 -5
- package/src/fields/Password/index.tsx +1 -1
- package/src/fields/Select/Select.hydration.spec.tsx +177 -0
- package/src/formEngine/doctypeMeta.ts +15 -114
- package/src/formEngine/index.ts +2 -0
- package/src/formEngine/seam.spec.ts +23 -4
- package/src/formEngine/useSessionRoles.ts +73 -0
- package/src/frappeConnections.ts +50 -0
- package/src/frappeNavigation.tsx +112 -0
- package/src/frappeRoutes.spec.tsx +335 -0
- package/src/index.ts +28 -1
- package/src/schemaForm/index.tsx +1 -0
- package/src/useConnectionFilters.tsx +157 -0
- package/src/useFrappePagination.ts +70 -26
- package/src/useFrappeRows.sharedPath.spec.tsx +206 -0
- package/src/useFrappeRows.ts +5 -4
- package/types/FrappeCalendar.d.ts.map +1 -1
- package/types/FrappeDashboard.d.ts +3 -4
- package/types/FrappeDashboard.d.ts.map +1 -1
- package/types/FrappeForm.d.ts +57 -5
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeFormConnections.d.ts +18 -0
- package/types/FrappeFormConnections.d.ts.map +1 -0
- package/types/FrappeImageView.d.ts +2 -3
- package/types/FrappeImageView.d.ts.map +1 -1
- package/types/FrappeList.d.ts +20 -2
- package/types/FrappeList.d.ts.map +1 -1
- package/types/FrappeListView.d.ts +53 -3
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/FrappeProvider.d.ts +8 -1
- package/types/FrappeProvider.d.ts.map +1 -1
- package/types/FrappeReportBuilder.d.ts.map +1 -1
- package/types/FrappeRowsReader.d.ts +26 -0
- package/types/FrappeRowsReader.d.ts.map +1 -0
- package/types/FrappeTable.d.ts +39 -8
- package/types/FrappeTable.d.ts.map +1 -1
- package/types/fieldRegistry.d.ts +17 -3
- package/types/fieldRegistry.d.ts.map +1 -1
- package/types/fields/Date/index.native.d.ts.map +1 -1
- package/types/fields/Date/sheetKeyboard.d.ts +4 -3
- package/types/fields/Date/sheetKeyboard.d.ts.map +1 -1
- package/types/fields/DateRange/index.d.ts.map +1 -1
- package/types/formEngine/doctypeMeta.d.ts +10 -114
- package/types/formEngine/doctypeMeta.d.ts.map +1 -1
- package/types/formEngine/index.d.ts +1 -0
- package/types/formEngine/index.d.ts.map +1 -1
- package/types/formEngine/useSessionRoles.d.ts +19 -0
- package/types/formEngine/useSessionRoles.d.ts.map +1 -0
- package/types/frappeConnections.d.ts +33 -0
- package/types/frappeConnections.d.ts.map +1 -0
- package/types/frappeNavigation.d.ts +49 -0
- package/types/frappeNavigation.d.ts.map +1 -0
- package/types/index.d.ts +7 -3
- package/types/index.d.ts.map +1 -1
- package/types/schemaForm/index.d.ts.map +1 -1
- package/types/useConnectionFilters.d.ts +34 -0
- package/types/useConnectionFilters.d.ts.map +1 -0
- package/types/useFrappePagination.d.ts +18 -5
- package/types/useFrappePagination.d.ts.map +1 -1
- package/types/useFrappeRows.d.ts +5 -4
- package/types/useFrappeRows.d.ts.map +1 -1
- package/src/FileBrowser/AttachLibrary.spec.tsx +0 -74
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { AsyncBoundary, PageSection } from "@multiplatform.one/components";
|
|
2
|
+
import { DataTable, type DataTableColumn } from "@multiplatform.one/table";
|
|
3
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import { YStack } from "tamagui";
|
|
7
|
+
import { connectionFields, type FrappeConnection } from "./frappeConnections";
|
|
8
|
+
import { isPermissionError } from "./liveErrors";
|
|
9
|
+
import { useFrappeRows, type FrappeRowsConnection } from "./useFrappeRows";
|
|
10
|
+
|
|
11
|
+
type ConnectionRow = Record<string, unknown> & { name: string; doctype: string };
|
|
12
|
+
|
|
13
|
+
const bookkeepingKeys = new Set([
|
|
14
|
+
"name",
|
|
15
|
+
"doctype",
|
|
16
|
+
"owner",
|
|
17
|
+
"creation",
|
|
18
|
+
"modified",
|
|
19
|
+
"modified_by",
|
|
20
|
+
"docstatus",
|
|
21
|
+
"idx",
|
|
22
|
+
"parent",
|
|
23
|
+
"parentfield",
|
|
24
|
+
"parenttype",
|
|
25
|
+
"_user_tags",
|
|
26
|
+
"_comments",
|
|
27
|
+
"_assign",
|
|
28
|
+
"_liked_by",
|
|
29
|
+
"_seen",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** "poke_type" -> "Poke Type". */
|
|
33
|
+
function fieldLabel(key: string): string {
|
|
34
|
+
return key.replace(/_/g, " ").replace(/(^|\s)\S/g, (c) => c.toUpperCase());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Columns for one connection's rows: its declared fields, else what the rows
|
|
39
|
+
* carry. The link field is left out; every row holds the same record.
|
|
40
|
+
*/
|
|
41
|
+
function connectionColumns(
|
|
42
|
+
link: FrappeConnection,
|
|
43
|
+
rows: readonly ConnectionRow[],
|
|
44
|
+
): DataTableColumn<ConnectionRow>[] {
|
|
45
|
+
const declared = (link.fields ?? []).filter((field) => field !== "name");
|
|
46
|
+
const keys = (
|
|
47
|
+
declared.length > 0
|
|
48
|
+
? declared
|
|
49
|
+
: Object.keys(rows[0] ?? {}).filter((key) => !bookkeepingKeys.has(key))
|
|
50
|
+
).filter((key) => key !== link.fieldname);
|
|
51
|
+
return (keys.length > 0 ? keys : ["name"]).map((key) => ({
|
|
52
|
+
id: key,
|
|
53
|
+
accessorKey: key,
|
|
54
|
+
header: fieldLabel(key),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FrappeFormConnectionSectionProps {
|
|
59
|
+
connection: FrappeRowsConnection | undefined;
|
|
60
|
+
link: FrappeConnection;
|
|
61
|
+
/** The record whose linked rows to read. */
|
|
62
|
+
name: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One connection of a record: its linked rows, live, with honest states. */
|
|
66
|
+
export function FrappeFormConnectionSection({
|
|
67
|
+
connection,
|
|
68
|
+
link,
|
|
69
|
+
name,
|
|
70
|
+
}: FrappeFormConnectionSectionProps) {
|
|
71
|
+
const { t } = useTranslation();
|
|
72
|
+
const { knobProps } = useResolvedKnobs();
|
|
73
|
+
const options = useMemo(
|
|
74
|
+
() => ({
|
|
75
|
+
fields: connectionFields(link),
|
|
76
|
+
filters: [[link.fieldname, "=", name]] as Array<[string, string, unknown]>,
|
|
77
|
+
limit: link.limit ?? 0,
|
|
78
|
+
}),
|
|
79
|
+
[link, name],
|
|
80
|
+
);
|
|
81
|
+
const { rows, isLoading, error } = useFrappeRows<ConnectionRow>(
|
|
82
|
+
connection,
|
|
83
|
+
link.doctype,
|
|
84
|
+
options as never,
|
|
85
|
+
);
|
|
86
|
+
const columns = useMemo(() => connectionColumns(link, rows), [link, rows]);
|
|
87
|
+
const title = t(link.label ?? link.doctype);
|
|
88
|
+
return (
|
|
89
|
+
<PageSection
|
|
90
|
+
title={isLoading || error ? title : `${title} (${rows.length})`}
|
|
91
|
+
divider={false}
|
|
92
|
+
testID={`frappe-form-connection-${link.doctype.toLowerCase().replace(/\s+/g, "-")}`}
|
|
93
|
+
>
|
|
94
|
+
<YStack {...knobProps.gap}>
|
|
95
|
+
<AsyncBoundary
|
|
96
|
+
layout="table"
|
|
97
|
+
compact
|
|
98
|
+
loading={isLoading}
|
|
99
|
+
error={error}
|
|
100
|
+
errorTitle={
|
|
101
|
+
error && isPermissionError(error)
|
|
102
|
+
? t("You do not have permission to view {{doctype}}", { doctype: link.doctype })
|
|
103
|
+
: t("Error loading {{doctype}}", { doctype: link.doctype })
|
|
104
|
+
}
|
|
105
|
+
empty={rows.length === 0}
|
|
106
|
+
emptyTitle={t("No {{doctype}} rows", { doctype: link.doctype })}
|
|
107
|
+
>
|
|
108
|
+
<DataTable<ConnectionRow>
|
|
109
|
+
data={rows}
|
|
110
|
+
columns={columns}
|
|
111
|
+
enableFiltering={false}
|
|
112
|
+
enableGlobalSearch={false}
|
|
113
|
+
enablePagination={false}
|
|
114
|
+
enableColumnVisibility={false}
|
|
115
|
+
enableViews={false}
|
|
116
|
+
/>
|
|
117
|
+
</AsyncBoundary>
|
|
118
|
+
</YStack>
|
|
119
|
+
</PageSection>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface FrappeFormConnectionsProps {
|
|
124
|
+
connection: FrappeRowsConnection | undefined;
|
|
125
|
+
connections: readonly FrappeConnection[];
|
|
126
|
+
name: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The desk's Connections for one record: a section per linked doctype. */
|
|
130
|
+
export function FrappeFormConnections({
|
|
131
|
+
connection,
|
|
132
|
+
connections,
|
|
133
|
+
name,
|
|
134
|
+
}: FrappeFormConnectionsProps) {
|
|
135
|
+
const { knobProps } = useResolvedKnobs();
|
|
136
|
+
return (
|
|
137
|
+
<YStack {...knobProps.gapLg} testID="frappe-form-connections">
|
|
138
|
+
{connections.map((link) => (
|
|
139
|
+
<FrappeFormConnectionSection
|
|
140
|
+
key={`${link.doctype}:${link.fieldname}`}
|
|
141
|
+
connection={connection}
|
|
142
|
+
link={link}
|
|
143
|
+
name={name}
|
|
144
|
+
/>
|
|
145
|
+
))}
|
|
146
|
+
</YStack>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// A signed-out visitor reads Guest-readable rows, but getdoctype and the
|
|
2
|
+
// DocType document are closed to Guest (MPO-322): pokemon's list and detail
|
|
3
|
+
// each logged a 403 for meta they render without. The bench below answers
|
|
4
|
+
// meta only to the signed-in bearer token, like the live tenant.
|
|
5
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
6
|
+
import { cleanup, screen, waitFor } from "@testing-library/react";
|
|
7
|
+
import i18next from "i18next";
|
|
8
|
+
import { initReactI18next } from "react-i18next";
|
|
9
|
+
import { Text } from "tamagui";
|
|
10
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { FrappeForm } from "./FrappeForm";
|
|
12
|
+
import { FrappeListView } from "./FrappeListView";
|
|
13
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
14
|
+
|
|
15
|
+
interface Pokemon {
|
|
16
|
+
name: string;
|
|
17
|
+
doctype: string;
|
|
18
|
+
pokemon_name: string;
|
|
19
|
+
poke_id: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fields = ["name", "pokemon_name", "poke_id"] as const;
|
|
23
|
+
const roster = [
|
|
24
|
+
{ name: "bulbasaur", pokemon_name: "Bulbasaur", poke_id: 1 },
|
|
25
|
+
{ name: "ivysaur", pokemon_name: "Ivysaur", poke_id: 2 },
|
|
26
|
+
];
|
|
27
|
+
const pokemonMeta = {
|
|
28
|
+
name: "Pokemon",
|
|
29
|
+
title_field: "pokemon_name",
|
|
30
|
+
fields: [{ fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name" }],
|
|
31
|
+
permissions: [{ role: "Guest", read: 1 }],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const signedOut = { useToken: true, type: "Bearer", token: async () => "" };
|
|
35
|
+
const signedIn = { useToken: true, type: "Bearer", token: async () => "jwt" };
|
|
36
|
+
|
|
37
|
+
let run = 0;
|
|
38
|
+
let bench = "";
|
|
39
|
+
let metaReads: string[] = [];
|
|
40
|
+
|
|
41
|
+
function json(body: unknown, status = 200) {
|
|
42
|
+
return new Response(JSON.stringify(body), {
|
|
43
|
+
status,
|
|
44
|
+
headers: { "content-type": "application/json" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stubBench() {
|
|
49
|
+
metaReads = [];
|
|
50
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request, init?: RequestInit) => {
|
|
51
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
52
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : {}));
|
|
53
|
+
if (
|
|
54
|
+
url.pathname === "/api/method/frappe.desk.form.load.getdoctype" ||
|
|
55
|
+
url.pathname.startsWith("/api/resource/DocType/")
|
|
56
|
+
) {
|
|
57
|
+
metaReads.push(url.pathname);
|
|
58
|
+
return headers.get("authorization") === "Bearer jwt"
|
|
59
|
+
? json({ docs: [pokemonMeta] })
|
|
60
|
+
: json({ exc_type: "PermissionError" }, 403);
|
|
61
|
+
}
|
|
62
|
+
if (url.pathname === "/api/resource/Pokemon") return json({ data: roster });
|
|
63
|
+
if (url.pathname === "/api/resource/Pokemon/bulbasaur") {
|
|
64
|
+
return json({ data: { ...roster[0], doctype: "Pokemon" } });
|
|
65
|
+
}
|
|
66
|
+
return json({ data: [] });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeAll(async () => {
|
|
71
|
+
await i18next.use(initReactI18next).init({
|
|
72
|
+
lng: "en",
|
|
73
|
+
interpolation: { escapeValue: false },
|
|
74
|
+
resources: { en: { translation: {} } },
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
run += 1;
|
|
80
|
+
bench = `http://guest-meta-${run}.spec.test`;
|
|
81
|
+
stubBench();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
cleanup();
|
|
86
|
+
vi.unstubAllGlobals();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function settle() {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, 50));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cardGrid(auth: typeof signedOut) {
|
|
94
|
+
return (
|
|
95
|
+
<FrappeProvider baseURL={bench} realtime={false} auth={auth}>
|
|
96
|
+
<FrappeListView<Pokemon>
|
|
97
|
+
doctype="Pokemon"
|
|
98
|
+
fields={fields}
|
|
99
|
+
viewMode="grid"
|
|
100
|
+
renderCard={(pokemon) => <Text>{pokemon.pokemon_name}</Text>}
|
|
101
|
+
showToolbar={false}
|
|
102
|
+
showSearch={false}
|
|
103
|
+
/>
|
|
104
|
+
</FrappeProvider>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("DocType meta for a signed-out visitor", () => {
|
|
109
|
+
it("the card grid renders its rows without asking for meta", async () => {
|
|
110
|
+
renderWithProviders(cardGrid(signedOut));
|
|
111
|
+
await waitFor(() => expect(screen.getByText("Ivysaur")).toBeTruthy());
|
|
112
|
+
await settle();
|
|
113
|
+
expect(metaReads).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("a render-prop form renders its doc without asking for meta", async () => {
|
|
117
|
+
renderWithProviders(
|
|
118
|
+
<FrappeProvider baseURL={bench} realtime={false} auth={signedOut}>
|
|
119
|
+
<FrappeForm<Pokemon>
|
|
120
|
+
doctype="Pokemon"
|
|
121
|
+
name="bulbasaur"
|
|
122
|
+
connections={[]}
|
|
123
|
+
showToolbar={false}
|
|
124
|
+
showSidebar={false}
|
|
125
|
+
showTimeline={false}
|
|
126
|
+
>
|
|
127
|
+
{({ doc }) => <Text>{`read ${doc.pokemon_name ?? ""}`}</Text>}
|
|
128
|
+
</FrappeForm>
|
|
129
|
+
</FrappeProvider>,
|
|
130
|
+
);
|
|
131
|
+
await waitFor(() => expect(screen.getByText("read Bulbasaur")).toBeTruthy());
|
|
132
|
+
await settle();
|
|
133
|
+
expect(metaReads).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("a signed-in reader still loads meta", async () => {
|
|
137
|
+
renderWithProviders(cardGrid(signedIn));
|
|
138
|
+
await waitFor(() => expect(screen.getByText("Ivysaur")).toBeTruthy());
|
|
139
|
+
await waitFor(() =>
|
|
140
|
+
expect(metaReads).toContain("/api/method/frappe.desk.form.load.getdoctype"),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
package/src/FrappeImageView.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useDocTypeMeta
|
|
1
|
+
import { useDocTypeMeta } from "@multiplatform.one/frappe";
|
|
2
2
|
import { AsyncSkeleton, Card, ImageGrid } from "@multiplatform.one/components";
|
|
3
3
|
import { ImageSquareIcon } from "@phosphor-icons/react";
|
|
4
4
|
import React, { useMemo } from "react";
|
|
@@ -9,6 +9,7 @@ import { isServer } from "@multiplatform.one/platform";
|
|
|
9
9
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
10
10
|
import { useFrappeConfig } from "./FrappeProvider";
|
|
11
11
|
import { FrappeReconnectIndicator } from "./FrappeReconnectIndicator";
|
|
12
|
+
import { useFrappeRows } from "./useFrappeRows";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* True when this module is evaluated during web server rendering.
|
|
@@ -66,9 +67,8 @@ export interface FrappeImageViewProps<TDoc extends { name: string; doctype: stri
|
|
|
66
67
|
/**
|
|
67
68
|
* Thin Frappe wrapper around the generic ImageGrid component.
|
|
68
69
|
*
|
|
69
|
-
* Fetches data via `
|
|
70
|
-
*
|
|
71
|
-
* `@multiplatform.one/components`.
|
|
70
|
+
* Fetches data via `useFrappeRows`, resolves image URLs, and passes items
|
|
71
|
+
* to the generic `ImageGrid` from `@multiplatform.one/components`.
|
|
72
72
|
*
|
|
73
73
|
* **SSR**: `useLiveQuery` (via `useSyncExternalStore`) lacks a
|
|
74
74
|
* `getServerSnapshot` so it can't run on the server. If the consumer
|
|
@@ -150,7 +150,7 @@ export function FrappeImageView<TDoc extends { name: string; doctype: string }>(
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Inner client-only implementation that calls the hooks requiring a
|
|
153
|
-
* browser environment (
|
|
153
|
+
* browser environment (useFrappeRows).
|
|
154
154
|
*/
|
|
155
155
|
function FrappeImageViewClient<TDoc extends { name: string; doctype: string }>({
|
|
156
156
|
doctype,
|
|
@@ -180,7 +180,7 @@ function FrappeImageViewClient<TDoc extends { name: string; doctype: string }>({
|
|
|
180
180
|
throw new Error("FrappeImageView requires baseURL prop or FrappeProvider with baseURL");
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
// Memoize so downstream hooks (useDocTypeMeta /
|
|
183
|
+
// Memoize so downstream hooks (useDocTypeMeta / useFrappeRows) receive
|
|
184
184
|
// a stable config reference across renders instead of a fresh object each
|
|
185
185
|
// time (which would otherwise retrigger their effects every render).
|
|
186
186
|
const appConfig = useMemo(
|
|
@@ -240,15 +240,11 @@ function FrappeImageViewClient<TDoc extends { name: string; doctype: string }>({
|
|
|
240
240
|
return searchFilter as unknown as typeof filters;
|
|
241
241
|
}, [filters, searchText, effectiveTitleField]);
|
|
242
242
|
|
|
243
|
-
const
|
|
243
|
+
const { rows: items, isQueryLoading: isDataLoading } = useFrappeRows<TDoc>(appConfig, doctype, {
|
|
244
244
|
fields: effectiveFields,
|
|
245
245
|
filters: effectiveFilters,
|
|
246
|
+
limit: null,
|
|
246
247
|
});
|
|
247
|
-
const { data: rawData, isLoading: isDataLoading } = useLiveQuery(
|
|
248
|
-
(q) => (collection ? q.from({ _list: collection }) : null),
|
|
249
|
-
[collection],
|
|
250
|
-
);
|
|
251
|
-
const items = useMemo(() => (rawData as TDoc[] | undefined) ?? [], [rawData]);
|
|
252
248
|
|
|
253
249
|
// Only block rendering on DocType-meta loading when we actually need it
|
|
254
250
|
// (i.e. the caller didn't pass explicit image/title fields). The meta
|
package/src/FrappeList.tsx
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AsyncBoundary,
|
|
3
|
+
ImageGrid,
|
|
4
|
+
List,
|
|
5
|
+
type ImageGridProps,
|
|
6
|
+
type ListProps,
|
|
7
|
+
} from "@multiplatform.one/components";
|
|
2
8
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
3
9
|
import type React from "react";
|
|
4
|
-
import { useMemo } from "react";
|
|
10
|
+
import { useCallback, useMemo } from "react";
|
|
5
11
|
import { useTranslation } from "react-i18next";
|
|
6
12
|
import { YStack } from "tamagui";
|
|
7
13
|
import { useFrappeConfig } from "./FrappeProvider";
|
|
8
14
|
import { FrappeReconnectIndicator } from "./FrappeReconnectIndicator";
|
|
9
15
|
import { isPermissionError } from "./liveErrors";
|
|
10
|
-
import { useFrappePagination } from "./useFrappePagination";
|
|
16
|
+
import { useFrappePagination, type UseFrappePaginationOptions } from "./useFrappePagination";
|
|
11
17
|
|
|
12
18
|
export interface FrappeListProps<TDoc extends { name: string; doctype: string }> extends Omit<
|
|
13
19
|
ListProps<Partial<TDoc>>,
|
|
@@ -21,8 +27,25 @@ export interface FrappeListProps<TDoc extends { name: string; doctype: string }>
|
|
|
21
27
|
doctype: string;
|
|
22
28
|
fields?: readonly (keyof TDoc & string)[];
|
|
23
29
|
filters?: Partial<TDoc> | Array<[keyof TDoc & string, string, any]>;
|
|
30
|
+
/** Rows the subscription reads; omitted, the bench's page length (20) applies. */
|
|
31
|
+
limit?: number;
|
|
32
|
+
orderBy?: UseFrappePaginationOptions<TDoc>["orderBy"];
|
|
33
|
+
/** Server-rendered rows for this same query, shown until the first sync lands. */
|
|
34
|
+
initialRows?: readonly Partial<TDoc>[];
|
|
24
35
|
/** Page size for progressive loading (default 20). */
|
|
25
36
|
pageSize?: number;
|
|
37
|
+
/**
|
|
38
|
+
* `grid` lays the rows out as cards through the catalog ImageGrid;
|
|
39
|
+
* `renderItem` then draws one card. Loading, error and empty stay the
|
|
40
|
+
* same states in both layouts.
|
|
41
|
+
*/
|
|
42
|
+
layout?: "list" | "grid";
|
|
43
|
+
/** Grid layout: column count, tile rung and media aspect of the cards. */
|
|
44
|
+
gridProps?: Pick<ImageGridProps<Partial<TDoc>>, "columns" | "tileSize" | "mediaAspect">;
|
|
45
|
+
/** Replaces the default empty message when the doctype has no rows to show. */
|
|
46
|
+
emptyState?: React.ReactNode;
|
|
47
|
+
/** Error-state retry, e.g. a remount that starts one fresh sync. */
|
|
48
|
+
onRetry?: () => void;
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
/**
|
|
@@ -39,8 +62,15 @@ export function FrappeList<TDoc extends { name: string; doctype: string }>({
|
|
|
39
62
|
doctype,
|
|
40
63
|
fields,
|
|
41
64
|
filters,
|
|
65
|
+
limit,
|
|
66
|
+
orderBy,
|
|
67
|
+
initialRows,
|
|
42
68
|
pageSize,
|
|
43
69
|
renderItem,
|
|
70
|
+
layout = "list",
|
|
71
|
+
gridProps,
|
|
72
|
+
emptyState,
|
|
73
|
+
onRetry,
|
|
44
74
|
...listProps
|
|
45
75
|
}: FrappeListProps<TDoc>) {
|
|
46
76
|
const config = useFrappeConfig();
|
|
@@ -85,7 +115,16 @@ export function FrappeList<TDoc extends { name: string; doctype: string }>({
|
|
|
85
115
|
const { items, isLoading, onLoadMore, hasMore, error } = useFrappePagination<TDoc>(
|
|
86
116
|
connectionConfig,
|
|
87
117
|
doctype,
|
|
88
|
-
{ fields, filters, pageSize },
|
|
118
|
+
{ fields, filters, pageSize, limit, orderBy, initialRows: initialRows as readonly TDoc[] },
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const itemIndex = useMemo(
|
|
122
|
+
() => new Map<Partial<TDoc>, number>(items.map((item, index) => [item, index])),
|
|
123
|
+
[items],
|
|
124
|
+
);
|
|
125
|
+
const renderCard = useCallback(
|
|
126
|
+
(item: Partial<TDoc>) => renderItem(item, itemIndex.get(item) ?? 0),
|
|
127
|
+
[renderItem, itemIndex],
|
|
89
128
|
);
|
|
90
129
|
|
|
91
130
|
// Honest error state: a permission/network failure must not masquerade
|
|
@@ -93,8 +132,9 @@ export function FrappeList<TDoc extends { name: string; doctype: string }>({
|
|
|
93
132
|
if (error) {
|
|
94
133
|
return (
|
|
95
134
|
<AsyncBoundary
|
|
96
|
-
layout="list"
|
|
135
|
+
layout={layout === "grid" ? "generic" : "list"}
|
|
97
136
|
error={error}
|
|
137
|
+
onRetry={onRetry}
|
|
98
138
|
errorTitle={
|
|
99
139
|
isPermissionError(error)
|
|
100
140
|
? t("You do not have permission to view {{doctype}}", { doctype })
|
|
@@ -105,7 +145,7 @@ export function FrappeList<TDoc extends { name: string; doctype: string }>({
|
|
|
105
145
|
}
|
|
106
146
|
|
|
107
147
|
return (
|
|
108
|
-
<YStack {...knobProps.gap} flex={1}>
|
|
148
|
+
<YStack {...knobProps.gap} flex={layout === "grid" ? undefined : 1}>
|
|
109
149
|
<FrappeReconnectIndicator config={connectionConfig} />
|
|
110
150
|
{/*
|
|
111
151
|
No chrome fragment rides this passthrough (LC-55 FRAGMENT-LICENSE):
|
|
@@ -114,15 +154,31 @@ export function FrappeList<TDoc extends { name: string; doctype: string }>({
|
|
|
114
154
|
clip. Rows and the error/empty panels own their own chrome; the error
|
|
115
155
|
branch above renders a bare AsyncBoundary, so both paths now match.
|
|
116
156
|
*/}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
157
|
+
{!isLoading && items.length === 0 && emptyState ? (
|
|
158
|
+
emptyState
|
|
159
|
+
) : layout === "grid" ? (
|
|
160
|
+
<ImageGrid
|
|
161
|
+
items={items}
|
|
162
|
+
isLoading={isLoading}
|
|
163
|
+
renderItem={renderCard}
|
|
164
|
+
onItemClick={listProps.onItemClick}
|
|
165
|
+
onLoadMore={onLoadMore}
|
|
166
|
+
hasMore={hasMore}
|
|
167
|
+
emptyMessage={listProps.emptyMessage ?? t("No {{doctype}} records found", { doctype })}
|
|
168
|
+
testID={listProps.testID}
|
|
169
|
+
{...gridProps}
|
|
170
|
+
/>
|
|
171
|
+
) : (
|
|
172
|
+
<List
|
|
173
|
+
items={items}
|
|
174
|
+
isLoading={isLoading}
|
|
175
|
+
renderItem={renderItem}
|
|
176
|
+
onLoadMore={onLoadMore}
|
|
177
|
+
hasMore={hasMore}
|
|
178
|
+
emptyMessage={t("No {{doctype}} records found", { doctype })}
|
|
179
|
+
{...listProps}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
126
182
|
</YStack>
|
|
127
183
|
);
|
|
128
184
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// FrappeListView's card slot (MPO-331): a doctype with a picture keeps its
|
|
2
|
+
// card without leaving the component. `renderCard` draws each row into the
|
|
3
|
+
// catalog ImageGrid; loading, empty and error stay the view's own states.
|
|
4
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
5
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
6
|
+
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
7
|
+
import i18next from "i18next";
|
|
8
|
+
import { initReactI18next } from "react-i18next";
|
|
9
|
+
import { Text } from "tamagui";
|
|
10
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import {
|
|
12
|
+
createRosterFixtures,
|
|
13
|
+
rosterPokemon,
|
|
14
|
+
rosterPokemonFields,
|
|
15
|
+
rosterPokemonMeta,
|
|
16
|
+
type RosterPokemon,
|
|
17
|
+
} from "../tests/fixtures/pokemonRoster";
|
|
18
|
+
import { FrappeListView, type FrappeListViewProps } from "./FrappeListView";
|
|
19
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
20
|
+
|
|
21
|
+
const host = "https://listview-cards.spec.test";
|
|
22
|
+
const realFetch = globalThis.fetch;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
await i18next.use(initReactI18next).init({
|
|
26
|
+
lng: "en",
|
|
27
|
+
interpolation: { escapeValue: false },
|
|
28
|
+
resources: { en: { translation: {} } },
|
|
29
|
+
});
|
|
30
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
31
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
32
|
+
if (url.startsWith(host)) {
|
|
33
|
+
const body = /\/api\/resource\/DocType\//.test(url)
|
|
34
|
+
? { data: rosterPokemonMeta }
|
|
35
|
+
: { data: [] };
|
|
36
|
+
return new Response(JSON.stringify(body), {
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return realFetch(input as RequestInfo, init);
|
|
42
|
+
}) as typeof fetch;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
globalThis.fetch = realFetch;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
cleanup();
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const card = (pokemon: Partial<RosterPokemon>, index: number) => (
|
|
55
|
+
<Text testID={`card-${pokemon.name}`}>{`${index}:${pokemon.pokemon_name}`}</Text>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
function renderCards(
|
|
59
|
+
fixtures: InMemoryFixtureProvider,
|
|
60
|
+
props: Partial<FrappeListViewProps<RosterPokemon>> = {},
|
|
61
|
+
) {
|
|
62
|
+
return renderWithProviders(
|
|
63
|
+
<FrappeProvider baseURL={host} fixtures={fixtures}>
|
|
64
|
+
<FrappeListView<RosterPokemon>
|
|
65
|
+
doctype="Pokemon"
|
|
66
|
+
fields={rosterPokemonFields}
|
|
67
|
+
viewMode="grid"
|
|
68
|
+
renderCard={card}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
</FrappeProvider>,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("FrappeListView card slot", () => {
|
|
76
|
+
it("draws every row through renderCard in the grid", async () => {
|
|
77
|
+
renderCards(createRosterFixtures());
|
|
78
|
+
for (const pokemon of rosterPokemon) {
|
|
79
|
+
await waitFor(() => expect(screen.getByTestId(`card-${pokemon.name}`)).toBeTruthy());
|
|
80
|
+
}
|
|
81
|
+
expect(screen.getByTestId("card-bulbasaur").textContent).toMatch(/^\d+:bulbasaur$/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("hands the slot each row's position in the page", async () => {
|
|
85
|
+
renderCards(createRosterFixtures());
|
|
86
|
+
await waitFor(() => expect(screen.getByTestId("card-eevee")).toBeTruthy());
|
|
87
|
+
const positions = rosterPokemon.map((pokemon) =>
|
|
88
|
+
Number(screen.getByTestId(`card-${pokemon.name}`).textContent?.split(":")[0]),
|
|
89
|
+
);
|
|
90
|
+
expect([...positions].sort((a, b) => a - b)).toEqual(rosterPokemon.map((_, i) => i));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("offers the card grid in the view switcher only when a slot is given", async () => {
|
|
94
|
+
const { unmount } = renderCards(createRosterFixtures(), { viewMode: undefined });
|
|
95
|
+
await waitFor(() => expect(screen.getByText("Cards")).toBeTruthy());
|
|
96
|
+
unmount();
|
|
97
|
+
renderWithProviders(
|
|
98
|
+
<FrappeProvider baseURL={host} fixtures={createRosterFixtures()}>
|
|
99
|
+
<FrappeListView<RosterPokemon> doctype="Pokemon" fields={rosterPokemonFields} />
|
|
100
|
+
</FrappeProvider>,
|
|
101
|
+
);
|
|
102
|
+
await waitFor(() => expect(screen.getByText("Table")).toBeTruthy());
|
|
103
|
+
expect(screen.queryByText("Cards")).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("a grid asked for without a slot falls back to the list", async () => {
|
|
107
|
+
renderWithProviders(
|
|
108
|
+
<FrappeProvider baseURL={host} fixtures={createRosterFixtures()}>
|
|
109
|
+
<FrappeListView<RosterPokemon>
|
|
110
|
+
doctype="Pokemon"
|
|
111
|
+
fields={rosterPokemonFields}
|
|
112
|
+
viewMode="grid"
|
|
113
|
+
displayFields={[{ key: "pokemon_name" }]}
|
|
114
|
+
/>
|
|
115
|
+
</FrappeProvider>,
|
|
116
|
+
);
|
|
117
|
+
await waitFor(() => expect(screen.getByText("pikachu")).toBeTruthy());
|
|
118
|
+
expect(screen.queryByTestId("card-pikachu")).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("switching views reports the mode and leaves a controlled view where it is", async () => {
|
|
122
|
+
const onViewModeChange = vi.fn();
|
|
123
|
+
renderCards(createRosterFixtures(), { onViewModeChange });
|
|
124
|
+
await waitFor(() => expect(screen.getByTestId("card-pikachu")).toBeTruthy());
|
|
125
|
+
fireEvent.click(screen.getByText("Table"));
|
|
126
|
+
expect(onViewModeChange).toHaveBeenCalledWith("table");
|
|
127
|
+
expect(screen.getByTestId("card-pikachu")).toBeTruthy();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("FrappeListView card grid states", () => {
|
|
132
|
+
it("renders the grid's skeleton while the first page loads", async () => {
|
|
133
|
+
const fixtures = createRosterFixtures();
|
|
134
|
+
let release: () => void = () => undefined;
|
|
135
|
+
const gate = new Promise<void>((resolve) => {
|
|
136
|
+
release = resolve;
|
|
137
|
+
});
|
|
138
|
+
const getList = fixtures.getList.bind(fixtures);
|
|
139
|
+
vi.spyOn(fixtures, "getList").mockImplementation(async (...args) => {
|
|
140
|
+
await gate;
|
|
141
|
+
return getList(...args);
|
|
142
|
+
});
|
|
143
|
+
renderCards(fixtures);
|
|
144
|
+
await waitFor(() =>
|
|
145
|
+
expect(
|
|
146
|
+
document.querySelector('[data-testid="image-grid-skeleton"], [aria-busy="true"]'),
|
|
147
|
+
).toBeTruthy(),
|
|
148
|
+
);
|
|
149
|
+
expect(screen.queryByTestId("card-pikachu")).toBeNull();
|
|
150
|
+
release();
|
|
151
|
+
await waitFor(() => expect(screen.getByTestId("card-pikachu")).toBeTruthy());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("an empty doctype shows the empty message, not a blank grid", async () => {
|
|
155
|
+
renderCards(createRosterFixtures({ Pokemon: [] }));
|
|
156
|
+
await waitFor(() => expect(screen.getByText("No Pokemon records found")).toBeTruthy());
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("an empty doctype shows the screen's own empty state when one is given", async () => {
|
|
160
|
+
renderCards(createRosterFixtures({ Pokemon: [] }), {
|
|
161
|
+
emptyState: <Text testID="roster-empty">Catch your first Pokemon</Text>,
|
|
162
|
+
});
|
|
163
|
+
await waitFor(() => expect(screen.getByTestId("roster-empty")).toBeTruthy());
|
|
164
|
+
expect(screen.queryByText("No Pokemon records found")).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("a failed read is an error with a retry, never an empty grid", async () => {
|
|
168
|
+
const fixtures = createRosterFixtures();
|
|
169
|
+
vi.spyOn(fixtures, "getList").mockRejectedValue(new Error("Failed to fetch"));
|
|
170
|
+
renderCards(fixtures, { emptyState: <Text testID="roster-empty">empty</Text> });
|
|
171
|
+
await waitFor(() => expect(screen.getByText("Error loading Pokemon")).toBeTruthy());
|
|
172
|
+
expect(screen.queryByTestId("roster-empty")).toBeNull();
|
|
173
|
+
expect(screen.getByRole("button", { name: /retry/i })).toBeTruthy();
|
|
174
|
+
});
|
|
175
|
+
});
|