@multiplatform.one/frappe-ui 7.10.0 → 7.15.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/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/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/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/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/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/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 +26 -22
- package/src/FileBrowser/AttachLibrary.stories.tsx +23 -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.catchUp.spec.tsx +502 -0
- package/src/FrappeProvider.tsx +15 -3
- 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/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/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 +10 -3
- 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,131 @@
|
|
|
1
|
+
import { Card } from "@multiplatform.one/components";
|
|
2
|
+
import { action } from "@multiplatform.one/storybook";
|
|
3
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
4
|
+
import { Image, Text, YStack } from "tamagui";
|
|
5
|
+
import {
|
|
6
|
+
createRosterFixtures,
|
|
7
|
+
rosterPokemonFields,
|
|
8
|
+
rosterPokemonMeta,
|
|
9
|
+
type RosterPokemon,
|
|
10
|
+
} from "../tests/fixtures/pokemonRoster";
|
|
11
|
+
import { FrappeListView } from "./FrappeListView";
|
|
12
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
title: "FrappeUI/FrappeListView Cards",
|
|
16
|
+
component: FrappeListView,
|
|
17
|
+
tags: ["!test"],
|
|
18
|
+
parameters: {
|
|
19
|
+
status: { type: "stable" },
|
|
20
|
+
docs: {
|
|
21
|
+
description: {
|
|
22
|
+
component:
|
|
23
|
+
"FrappeListView's card grid: `renderCard` draws each row of the doctype into the catalog ImageGrid, while loading, empty and error stay the view's own states. Rows come from the pokemon fixtures (InMemoryFixtureProvider); DocType meta from an in-story fetch mock.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const host = "https://listview-cards.mock.test";
|
|
30
|
+
const errorHost = "https://error.listview-cards.mock.test";
|
|
31
|
+
|
|
32
|
+
(() => {
|
|
33
|
+
const w = window as unknown as Record<string, unknown>;
|
|
34
|
+
if (w.__frappeListViewCardsMockFetch) return;
|
|
35
|
+
w.__frappeListViewCardsMockFetch = true;
|
|
36
|
+
const realFetch = window.fetch.bind(window);
|
|
37
|
+
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
38
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
39
|
+
if (url.startsWith(host) || url.startsWith(errorHost)) {
|
|
40
|
+
const body = /\/api\/resource\/DocType\//.test(url)
|
|
41
|
+
? { data: rosterPokemonMeta }
|
|
42
|
+
: { data: [] };
|
|
43
|
+
return new Response(JSON.stringify(body), {
|
|
44
|
+
status: 200,
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return realFetch(input as RequestInfo, init);
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
|
|
52
|
+
function PokemonTile({ pokemon }: { pokemon: Partial<RosterPokemon> }) {
|
|
53
|
+
const { knobProps } = useResolvedKnobs();
|
|
54
|
+
return (
|
|
55
|
+
<Card>
|
|
56
|
+
<YStack {...knobProps.panelPadding} {...knobProps.gap} alignItems="center">
|
|
57
|
+
<Image
|
|
58
|
+
source={{ uri: pokemon.sprite_front, width: 96, height: 96 }}
|
|
59
|
+
alt={pokemon.pokemon_name ?? ""}
|
|
60
|
+
/>
|
|
61
|
+
<Text {...knobProps.body} fontWeight="700" textTransform="capitalize">
|
|
62
|
+
{pokemon.pokemon_name}
|
|
63
|
+
</Text>
|
|
64
|
+
<Text {...knobProps.label} color="$color10">
|
|
65
|
+
{`#${String(pokemon.poke_id ?? 0).padStart(3, "0")} · ${pokemon.species}`}
|
|
66
|
+
</Text>
|
|
67
|
+
</YStack>
|
|
68
|
+
</Card>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const renderCard = (pokemon: Partial<RosterPokemon>) => <PokemonTile pokemon={pokemon} />;
|
|
73
|
+
|
|
74
|
+
export const CardGrid = () => (
|
|
75
|
+
<FrappeProvider baseURL={host} fixtures={createRosterFixtures()}>
|
|
76
|
+
<FrappeListView<RosterPokemon>
|
|
77
|
+
doctype="Pokemon"
|
|
78
|
+
fields={rosterPokemonFields}
|
|
79
|
+
defaultViewMode="grid"
|
|
80
|
+
renderCard={renderCard}
|
|
81
|
+
onItemClick={action("onItemClick")}
|
|
82
|
+
onAdd={action("onAdd")}
|
|
83
|
+
/>
|
|
84
|
+
</FrappeProvider>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export const CardGridInPage = () => (
|
|
88
|
+
<FrappeProvider baseURL={host} fixtures={createRosterFixtures()}>
|
|
89
|
+
<FrappeListView<RosterPokemon>
|
|
90
|
+
doctype="Pokemon"
|
|
91
|
+
fields={rosterPokemonFields}
|
|
92
|
+
viewMode="grid"
|
|
93
|
+
renderCard={renderCard}
|
|
94
|
+
showToolbar={false}
|
|
95
|
+
showSearch={false}
|
|
96
|
+
surface={false}
|
|
97
|
+
pageSize={4}
|
|
98
|
+
onItemClick={action("onItemClick")}
|
|
99
|
+
/>
|
|
100
|
+
</FrappeProvider>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
export const EmptyRoster = () => (
|
|
104
|
+
<FrappeProvider baseURL={host} fixtures={createRosterFixtures({ Pokemon: [] })}>
|
|
105
|
+
<FrappeListView<RosterPokemon>
|
|
106
|
+
doctype="Pokemon"
|
|
107
|
+
fields={rosterPokemonFields}
|
|
108
|
+
viewMode="grid"
|
|
109
|
+
renderCard={renderCard}
|
|
110
|
+
/>
|
|
111
|
+
</FrappeProvider>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
function failingFixtures() {
|
|
115
|
+
const fixtures = createRosterFixtures();
|
|
116
|
+
fixtures.getList = async () => {
|
|
117
|
+
throw Object.assign(new Error("Not permitted"), { status: 403 });
|
|
118
|
+
};
|
|
119
|
+
return fixtures;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const PermissionDenied = () => (
|
|
123
|
+
<FrappeProvider baseURL={errorHost} fixtures={failingFixtures()}>
|
|
124
|
+
<FrappeListView<RosterPokemon>
|
|
125
|
+
doctype="Pokemon"
|
|
126
|
+
fields={rosterPokemonFields}
|
|
127
|
+
viewMode="grid"
|
|
128
|
+
renderCard={renderCard}
|
|
129
|
+
/>
|
|
130
|
+
</FrappeProvider>
|
|
131
|
+
);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// FrappeListView filters a doctype by a value held in a linked junction
|
|
2
|
+
// doctype (MPO-331): Pokemon by `Pokemon Poke Type.poke_type`. Types live in
|
|
3
|
+
// a standalone doctype with a `pokemon` Link, not a child table, so the view
|
|
4
|
+
// reads the matching junction rows itself and the app keeps no collection.
|
|
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 { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
createRosterFixtures,
|
|
12
|
+
rosterPokemonFields,
|
|
13
|
+
rosterPokemonMeta,
|
|
14
|
+
type RosterPokemon,
|
|
15
|
+
} from "../tests/fixtures/pokemonRoster";
|
|
16
|
+
import type { FrappeConnection } from "./frappeConnections";
|
|
17
|
+
import { connectionsFromMeta } from "./frappeConnections";
|
|
18
|
+
import { FrappeListView, type FrappeListViewProps } from "./FrappeListView";
|
|
19
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
20
|
+
|
|
21
|
+
const host = "https://listview-connections.spec.test";
|
|
22
|
+
const realFetch = globalThis.fetch;
|
|
23
|
+
let servedMeta: Record<string, unknown> = rosterPokemonMeta;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await i18next.use(initReactI18next).init({
|
|
27
|
+
lng: "en",
|
|
28
|
+
interpolation: { escapeValue: false },
|
|
29
|
+
resources: { en: { translation: {} } },
|
|
30
|
+
});
|
|
31
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
32
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
33
|
+
if (url.startsWith(host)) {
|
|
34
|
+
const body = /\/api\/resource\/DocType\//.test(url) ? { data: servedMeta } : { data: [] };
|
|
35
|
+
return new Response(JSON.stringify(body), {
|
|
36
|
+
status: 200,
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return realFetch(input as RequestInfo, init);
|
|
41
|
+
}) as typeof fetch;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
globalThis.fetch = realFetch;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
cleanup();
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
servedMeta = rosterPokemonMeta;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const typeConnection: FrappeConnection = { doctype: "Pokemon Poke Type", fieldname: "pokemon" };
|
|
55
|
+
|
|
56
|
+
function renderRoster(
|
|
57
|
+
props: Partial<FrappeListViewProps<RosterPokemon>>,
|
|
58
|
+
fixtures = createRosterFixtures(),
|
|
59
|
+
) {
|
|
60
|
+
const view = renderWithProviders(
|
|
61
|
+
<FrappeProvider baseURL={host} fixtures={fixtures}>
|
|
62
|
+
<FrappeListView<RosterPokemon>
|
|
63
|
+
doctype="Pokemon"
|
|
64
|
+
fields={rosterPokemonFields}
|
|
65
|
+
defaultViewMode="list"
|
|
66
|
+
showToolbar={false}
|
|
67
|
+
displayFields={[{ key: "pokemon_name" }]}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
</FrappeProvider>,
|
|
71
|
+
);
|
|
72
|
+
return { ...view, fixtures };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const listed = () =>
|
|
76
|
+
["bulbasaur", "charmander", "squirtle", "pikachu", "gengar", "eevee"].filter(
|
|
77
|
+
(name) => screen.queryByText(name) !== null,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
describe("FrappeListView connection filters", () => {
|
|
81
|
+
it("lists only the records a matching junction row names", async () => {
|
|
82
|
+
renderRoster({
|
|
83
|
+
connections: [typeConnection],
|
|
84
|
+
filters: [["Pokemon Poke Type", "poke_type", "=", "poison"]],
|
|
85
|
+
});
|
|
86
|
+
await waitFor(() => expect(listed()).toEqual(["bulbasaur", "gengar"]));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("reads the junction and the doctype itself, with no collection of the app's", async () => {
|
|
90
|
+
const fixtures = createRosterFixtures();
|
|
91
|
+
const getList = vi.spyOn(fixtures, "getList");
|
|
92
|
+
renderRoster(
|
|
93
|
+
{
|
|
94
|
+
connections: [typeConnection],
|
|
95
|
+
filters: [["Pokemon Poke Type", "poke_type", "=", "fire"]],
|
|
96
|
+
},
|
|
97
|
+
fixtures,
|
|
98
|
+
);
|
|
99
|
+
await waitFor(() => expect(listed()).toEqual(["charmander"]));
|
|
100
|
+
const calls = getList.mock.calls.map(([doctype, options]) => [doctype, options?.filters]);
|
|
101
|
+
expect(calls).toContainEqual(["Pokemon Poke Type", [["poke_type", "=", "fire"]]]);
|
|
102
|
+
expect(calls).toContainEqual(["Pokemon", [["name", "in", ["charmander"]]]]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("takes the connection from meta.links when none is declared", async () => {
|
|
106
|
+
renderRoster({ filters: [["Pokemon Poke Type", "poke_type", "=", "water"]] });
|
|
107
|
+
await waitFor(() => expect(listed()).toEqual(["squirtle"]));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("combines with the doctype's own filters", async () => {
|
|
111
|
+
renderRoster({
|
|
112
|
+
connections: [typeConnection],
|
|
113
|
+
filters: [
|
|
114
|
+
["base_experience", ">", 100],
|
|
115
|
+
["Pokemon Poke Type", "poke_type", "=", "poison"],
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
await waitFor(() => expect(listed()).toEqual(["gengar"]));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("follows a new filter value", async () => {
|
|
122
|
+
const fixtures = createRosterFixtures();
|
|
123
|
+
const { rerender } = renderRoster(
|
|
124
|
+
{ connections: [typeConnection], filters: [["Pokemon Poke Type", "poke_type", "=", "fire"]] },
|
|
125
|
+
fixtures,
|
|
126
|
+
);
|
|
127
|
+
await waitFor(() => expect(listed()).toEqual(["charmander"]));
|
|
128
|
+
rerender(
|
|
129
|
+
<FrappeProvider baseURL={host} fixtures={fixtures}>
|
|
130
|
+
<FrappeListView<RosterPokemon>
|
|
131
|
+
doctype="Pokemon"
|
|
132
|
+
fields={rosterPokemonFields}
|
|
133
|
+
defaultViewMode="list"
|
|
134
|
+
showToolbar={false}
|
|
135
|
+
displayFields={[{ key: "pokemon_name" }]}
|
|
136
|
+
connections={[typeConnection]}
|
|
137
|
+
filters={[["Pokemon Poke Type", "poke_type", "=", "electric"]]}
|
|
138
|
+
/>
|
|
139
|
+
</FrappeProvider>,
|
|
140
|
+
);
|
|
141
|
+
await waitFor(() => expect(listed()).toEqual(["pikachu"]));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("a value no junction row holds is an honest no-match, not the whole roster", async () => {
|
|
145
|
+
renderRoster({
|
|
146
|
+
connections: [typeConnection],
|
|
147
|
+
filters: [["Pokemon Poke Type", "poke_type", "=", "dragon"]],
|
|
148
|
+
});
|
|
149
|
+
await waitFor(() =>
|
|
150
|
+
expect(screen.getByText("No Pokemon records match the active filters")).toBeTruthy(),
|
|
151
|
+
);
|
|
152
|
+
expect(listed()).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("a junction the reader may not read is an error, not an empty list", async () => {
|
|
156
|
+
const fixtures = createRosterFixtures();
|
|
157
|
+
const getList = fixtures.getList.bind(fixtures);
|
|
158
|
+
vi.spyOn(fixtures, "getList").mockImplementation(async (doctype, options) => {
|
|
159
|
+
if (doctype === "Pokemon Poke Type") {
|
|
160
|
+
throw Object.assign(new Error("PermissionError"), { status: 403 });
|
|
161
|
+
}
|
|
162
|
+
return getList(doctype, options);
|
|
163
|
+
});
|
|
164
|
+
renderRoster(
|
|
165
|
+
{ connections: [typeConnection], filters: [["Pokemon Poke Type", "poke_type", "=", "fire"]] },
|
|
166
|
+
fixtures,
|
|
167
|
+
);
|
|
168
|
+
await waitFor(() =>
|
|
169
|
+
expect(screen.getByText("You do not have permission to view Pokemon")).toBeTruthy(),
|
|
170
|
+
);
|
|
171
|
+
expect(listed()).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("a four-part filter on the doctype itself is its own field", async () => {
|
|
175
|
+
renderRoster({ filters: [["Pokemon", "poke_id", ">", 90]] });
|
|
176
|
+
await waitFor(() => expect(listed()).toEqual(["gengar", "eevee"]));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("connectionsFromMeta", () => {
|
|
181
|
+
it("reads DocType Link rows, leaving out hidden and child-table links", () => {
|
|
182
|
+
expect(
|
|
183
|
+
connectionsFromMeta({
|
|
184
|
+
links: [
|
|
185
|
+
{ link_doctype: "Pokemon Poke Type", link_fieldname: "pokemon", group: "Battle" },
|
|
186
|
+
{ link_doctype: "Hidden", link_fieldname: "pokemon", hidden: 1 },
|
|
187
|
+
{ link_doctype: "Via Child", link_fieldname: "pokemon", is_child_table: 1 },
|
|
188
|
+
],
|
|
189
|
+
}),
|
|
190
|
+
).toEqual([{ doctype: "Pokemon Poke Type", fieldname: "pokemon", group: "Battle" }]);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Button } from "@multiplatform.one/forms";
|
|
2
|
+
import { action } from "@multiplatform.one/storybook";
|
|
3
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { XStack, YStack } from "tamagui";
|
|
6
|
+
import {
|
|
7
|
+
createRosterFixtures,
|
|
8
|
+
rosterPokemonFields,
|
|
9
|
+
rosterPokemonMeta,
|
|
10
|
+
type RosterPokemon,
|
|
11
|
+
} from "../tests/fixtures/pokemonRoster";
|
|
12
|
+
import type { FrappeConnection } from "./frappeConnections";
|
|
13
|
+
import { FrappeListView } from "./FrappeListView";
|
|
14
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
title: "FrappeUI/FrappeListView Connections",
|
|
18
|
+
component: FrappeListView,
|
|
19
|
+
tags: ["!test"],
|
|
20
|
+
parameters: {
|
|
21
|
+
status: { type: "stable" },
|
|
22
|
+
docs: {
|
|
23
|
+
description: {
|
|
24
|
+
component:
|
|
25
|
+
"A four-part filter on a connection, `[doctype, field, operator, value]`, narrows the list to the records whose linked rows match: Pokemon by `Pokemon Poke Type.poke_type`. The junction is a standalone doctype with a `pokemon` Link, so FrappeListView reads its matching rows and filters on their names; the story holds no collection of its own. Rows come from the pokemon fixtures.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const host = "https://listview-connections.mock.test";
|
|
32
|
+
|
|
33
|
+
(() => {
|
|
34
|
+
const w = window as unknown as Record<string, unknown>;
|
|
35
|
+
if (w.__frappeListViewConnectionsMockFetch) return;
|
|
36
|
+
w.__frappeListViewConnectionsMockFetch = true;
|
|
37
|
+
const realFetch = window.fetch.bind(window);
|
|
38
|
+
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
39
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
40
|
+
if (url.startsWith(host)) {
|
|
41
|
+
const body = /\/api\/resource\/DocType\//.test(url)
|
|
42
|
+
? { data: rosterPokemonMeta }
|
|
43
|
+
: { data: [] };
|
|
44
|
+
return new Response(JSON.stringify(body), {
|
|
45
|
+
status: 200,
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return realFetch(input as RequestInfo, init);
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
const pokemonConnections: FrappeConnection[] = [
|
|
54
|
+
{ doctype: "Pokemon Poke Type", fieldname: "pokemon" },
|
|
55
|
+
];
|
|
56
|
+
const types = ["", "grass", "poison", "fire", "water", "electric", "ghost", "normal"];
|
|
57
|
+
|
|
58
|
+
function TypeFilteredRoster({ declared }: { declared: boolean }) {
|
|
59
|
+
const { knobProps } = useResolvedKnobs();
|
|
60
|
+
const [type, setType] = useState("poison");
|
|
61
|
+
return (
|
|
62
|
+
<YStack {...knobProps.gap} flex={1}>
|
|
63
|
+
<XStack {...knobProps.gap} flexWrap="wrap">
|
|
64
|
+
{types.map((value) => (
|
|
65
|
+
<Button
|
|
66
|
+
key={value || "all"}
|
|
67
|
+
size="$3"
|
|
68
|
+
accent={value === type}
|
|
69
|
+
outlined={value !== type}
|
|
70
|
+
aria-pressed={value === type}
|
|
71
|
+
onPress={() => setType(value)}
|
|
72
|
+
>
|
|
73
|
+
{value || "all"}
|
|
74
|
+
</Button>
|
|
75
|
+
))}
|
|
76
|
+
</XStack>
|
|
77
|
+
<FrappeListView<RosterPokemon>
|
|
78
|
+
doctype="Pokemon"
|
|
79
|
+
fields={rosterPokemonFields}
|
|
80
|
+
connections={declared ? pokemonConnections : undefined}
|
|
81
|
+
filters={type ? [["Pokemon Poke Type", "poke_type", "=", type]] : undefined}
|
|
82
|
+
displayFields={[{ key: "pokemon_name" }, { key: "species" }]}
|
|
83
|
+
onItemClick={action("onItemClick")}
|
|
84
|
+
/>
|
|
85
|
+
</YStack>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const ByJunctionValue = () => (
|
|
90
|
+
<FrappeProvider baseURL={host} fixtures={createRosterFixtures()}>
|
|
91
|
+
<TypeFilteredRoster declared />
|
|
92
|
+
</FrappeProvider>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const ConnectionsFromMetaLinks = () => (
|
|
96
|
+
<FrappeProvider baseURL={host} fixtures={createRosterFixtures()}>
|
|
97
|
+
<TypeFilteredRoster declared={false} />
|
|
98
|
+
</FrappeProvider>
|
|
99
|
+
);
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// FrappeListView takes the rows a route loader rendered on the server and
|
|
2
|
+
// hands over to the live collection without a refetch that shrinks the page
|
|
3
|
+
// (MPO-331, after MPO-322's 21-to-20 shrink on pokemon). The bench below
|
|
4
|
+
// pages at 20 rows unless a request names `limit_page_length`, like a stock
|
|
5
|
+
// Frappe site, and holds 21 Pokemon. The card grid is the layout that
|
|
6
|
+
// server-renders its rows (the list layout virtualizes, so it paints after
|
|
7
|
+
// layout on the client).
|
|
8
|
+
import { loadDocList, Order } from "@multiplatform.one/frappe";
|
|
9
|
+
import { renderWithProviders, TestProviders } from "@multiplatform.one/test-utils";
|
|
10
|
+
import { act, cleanup, screen, waitFor } from "@testing-library/react";
|
|
11
|
+
import i18next from "i18next";
|
|
12
|
+
import { renderToString } from "react-dom/server";
|
|
13
|
+
import { initReactI18next } from "react-i18next";
|
|
14
|
+
import { Text } from "tamagui";
|
|
15
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
|
+
import { FrappeListView, type FrappeListViewProps } from "./FrappeListView";
|
|
17
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
18
|
+
|
|
19
|
+
interface Pokemon {
|
|
20
|
+
name: string;
|
|
21
|
+
doctype: string;
|
|
22
|
+
pokemon_name: string;
|
|
23
|
+
poke_id: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A bench per test: the collection layer shares one collection per query
|
|
27
|
+
// and origin, so a reused origin would hand a test the last test's rows.
|
|
28
|
+
let run = 0;
|
|
29
|
+
let bench = "";
|
|
30
|
+
const rosterSize = 21;
|
|
31
|
+
const fields = ["name", "pokemon_name", "poke_id"] as const;
|
|
32
|
+
const orderBy = [{ field: "poke_id" as const, order: Order.Asc }];
|
|
33
|
+
const pageQuery = { fields, orderBy, limit: 100 };
|
|
34
|
+
|
|
35
|
+
const roster = Array.from({ length: rosterSize }, (_, index) => ({
|
|
36
|
+
name: `pokemon-${String(index + 1).padStart(2, "0")}`,
|
|
37
|
+
pokemon_name: `mon-${String(index + 1).padStart(2, "0")}`,
|
|
38
|
+
poke_id: index + 1,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const pokemonMeta = {
|
|
42
|
+
name: "Pokemon",
|
|
43
|
+
title_field: "pokemon_name",
|
|
44
|
+
fields: [
|
|
45
|
+
{ fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", in_list_view: 1 },
|
|
46
|
+
{ fieldname: "poke_id", fieldtype: "Int", label: "Poke ID", in_list_view: 1 },
|
|
47
|
+
],
|
|
48
|
+
permissions: [{ role: "Guest", read: 1 }],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let listReads: URL[] = [];
|
|
52
|
+
let releaseReads: (() => void) | null = null;
|
|
53
|
+
|
|
54
|
+
function stubBench(options: { hold?: boolean; reversedNames?: boolean } = {}) {
|
|
55
|
+
listReads = [];
|
|
56
|
+
const held = options.hold
|
|
57
|
+
? new Promise<void>((resolve) => {
|
|
58
|
+
releaseReads = resolve;
|
|
59
|
+
})
|
|
60
|
+
: null;
|
|
61
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
62
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
63
|
+
if (url.pathname.startsWith("/api/resource/DocType/")) {
|
|
64
|
+
return new Response(JSON.stringify({ data: pokemonMeta }), {
|
|
65
|
+
status: 200,
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (url.pathname === "/api/resource/Pokemon") {
|
|
70
|
+
listReads.push(url);
|
|
71
|
+
if (held) await held;
|
|
72
|
+
const pageLength = Number(url.searchParams.get("limit_page_length") ?? 20);
|
|
73
|
+
const page = pageLength === 0 ? roster : roster.slice(0, pageLength);
|
|
74
|
+
// Names that sort against the Pokedex order, so a list that fell back
|
|
75
|
+
// to name order would show it.
|
|
76
|
+
const rows = options.reversedNames
|
|
77
|
+
? page.map((row) => ({
|
|
78
|
+
...row,
|
|
79
|
+
name: `pokemon-${String(rosterSize + 1 - row.poke_id).padStart(2, "0")}`,
|
|
80
|
+
}))
|
|
81
|
+
: page;
|
|
82
|
+
return new Response(JSON.stringify({ data: rows }), {
|
|
83
|
+
status: 200,
|
|
84
|
+
headers: { "content-type": "application/json" },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return new Response(JSON.stringify({ data: [] }), { status: 200 });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
beforeAll(async () => {
|
|
92
|
+
await i18next.use(initReactI18next).init({
|
|
93
|
+
lng: "en",
|
|
94
|
+
interpolation: { escapeValue: false },
|
|
95
|
+
resources: { en: { translation: {} } },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
run += 1;
|
|
101
|
+
bench = `http://seed-${run}.spec.test`;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
cleanup();
|
|
106
|
+
releaseReads?.();
|
|
107
|
+
releaseReads = null;
|
|
108
|
+
vi.unstubAllGlobals();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const renderedNames = () => roster.filter((row) => screen.queryByText(row.pokemon_name) !== null);
|
|
112
|
+
|
|
113
|
+
function listView(props: Partial<FrappeListViewProps<Pokemon>>) {
|
|
114
|
+
return (
|
|
115
|
+
<FrappeProvider baseURL={bench} realtime={false}>
|
|
116
|
+
<FrappeListView<Pokemon>
|
|
117
|
+
doctype="Pokemon"
|
|
118
|
+
fields={fields}
|
|
119
|
+
orderBy={orderBy}
|
|
120
|
+
viewMode="grid"
|
|
121
|
+
renderCard={(pokemon) => <Text>{pokemon.pokemon_name}</Text>}
|
|
122
|
+
showToolbar={false}
|
|
123
|
+
showSearch={false}
|
|
124
|
+
pageSize={50}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
</FrappeProvider>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function serverPage() {
|
|
132
|
+
const request = new Request(`${bench}/pokemon`, { headers: { cookie: "sid=guest" } });
|
|
133
|
+
return loadDocList<Pokemon>({ baseURL: bench, doctype: "Pokemon", request, ...pageQuery });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe("loadDocList", () => {
|
|
137
|
+
it("reads the page it is given and stamps the doctype", async () => {
|
|
138
|
+
stubBench();
|
|
139
|
+
const rows = await serverPage();
|
|
140
|
+
expect(rows).toHaveLength(rosterSize);
|
|
141
|
+
expect(rows.every((row) => row.doctype === "Pokemon")).toBe(true);
|
|
142
|
+
expect(listReads[0].searchParams.get("limit_page_length")).toBe("100");
|
|
143
|
+
expect(listReads[0].searchParams.get("order_by")).toBe("poke_id asc");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("reads nothing without a request (a loader run in the browser)", async () => {
|
|
147
|
+
stubBench();
|
|
148
|
+
expect(await loadDocList({ baseURL: bench, doctype: "Pokemon", ...pageQuery })).toEqual([]);
|
|
149
|
+
expect(listReads).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("FrappeListView SSR seed", () => {
|
|
154
|
+
it("renders the loader's rows on the server", async () => {
|
|
155
|
+
stubBench();
|
|
156
|
+
const initialRows = await serverPage();
|
|
157
|
+
const html = renderToString(
|
|
158
|
+
<TestProviders>{listView({ initialRows, limit: 100 })}</TestProviders>,
|
|
159
|
+
);
|
|
160
|
+
for (const row of roster) expect(html).toContain(row.pokemon_name);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("shows the loader's rows before the live collection answers", async () => {
|
|
164
|
+
stubBench();
|
|
165
|
+
const initialRows = await serverPage();
|
|
166
|
+
stubBench({ hold: true });
|
|
167
|
+
renderWithProviders(listView({ initialRows, limit: 100 }));
|
|
168
|
+
expect(renderedNames()).toHaveLength(rosterSize);
|
|
169
|
+
await waitFor(() => expect(listReads.length).toBeGreaterThan(0));
|
|
170
|
+
expect(renderedNames()).toHaveLength(rosterSize);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("hands over to the live collection reading the same page, never fewer rows", async () => {
|
|
174
|
+
stubBench();
|
|
175
|
+
const initialRows = await serverPage();
|
|
176
|
+
stubBench({ hold: true });
|
|
177
|
+
const counts: number[] = [];
|
|
178
|
+
const observer = new MutationObserver(() => counts.push(renderedNames().length));
|
|
179
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
180
|
+
renderWithProviders(listView({ initialRows, limit: 100 }));
|
|
181
|
+
await waitFor(() => expect(listReads.length).toBeGreaterThan(0));
|
|
182
|
+
await act(async () => {
|
|
183
|
+
releaseReads?.();
|
|
184
|
+
});
|
|
185
|
+
await waitFor(() =>
|
|
186
|
+
expect(listReads.every((url) => url.searchParams.get("limit_page_length") === "100")).toBe(
|
|
187
|
+
true,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
191
|
+
observer.disconnect();
|
|
192
|
+
expect(renderedNames()).toHaveLength(rosterSize);
|
|
193
|
+
expect(Math.min(...counts.filter((count) => count > 0))).toBe(rosterSize);
|
|
194
|
+
expect(listReads[0].searchParams.get("order_by")).toBe("poke_id asc");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("lists the live rows in the order it read them, not by name", async () => {
|
|
198
|
+
stubBench({ reversedNames: true });
|
|
199
|
+
renderWithProviders(listView({ limit: 100 }));
|
|
200
|
+
await waitFor(() => expect(renderedNames()).toHaveLength(rosterSize));
|
|
201
|
+
const shown = [...document.body.querySelectorAll("span, div")]
|
|
202
|
+
.map((node) => node.textContent ?? "")
|
|
203
|
+
.filter((text) => /^mon-\d\d$/.test(text));
|
|
204
|
+
expect([...new Set(shown)]).toEqual(roster.map((row) => row.pokemon_name));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("without the loader's limit the live page is the bench's 20 (the shrink this guards)", async () => {
|
|
208
|
+
stubBench();
|
|
209
|
+
const initialRows = await serverPage();
|
|
210
|
+
stubBench();
|
|
211
|
+
renderWithProviders(listView({ initialRows }));
|
|
212
|
+
await waitFor(() => expect(renderedNames()).toHaveLength(20));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("keeps the server rows when the live read fails", async () => {
|
|
216
|
+
stubBench();
|
|
217
|
+
const initialRows = await serverPage();
|
|
218
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
219
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
220
|
+
if (url.pathname === "/api/resource/Pokemon") throw new TypeError("Failed to fetch");
|
|
221
|
+
return new Response(JSON.stringify({ data: pokemonMeta }), { status: 200 });
|
|
222
|
+
});
|
|
223
|
+
renderWithProviders(listView({ initialRows, limit: 100 }));
|
|
224
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
225
|
+
expect(renderedNames()).toHaveLength(rosterSize);
|
|
226
|
+
expect(screen.queryByText("Error loading Pokemon")).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|